Merge pull request #129 from theta42/feat/super-admin-overview-rename

Add cross-app super admin group; rename Executive page to Overview
This commit is contained in:
2026-07-30 12:00:23 -04:00
committed by GitHub
11 changed files with 33 additions and 22 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ on, just like anyone else's.
A **group** is just a named list of accounts, used to control access. This A **group** is just a named list of accounts, used to control access. This
app has a handful of built-in groups that grant admin powers (e.g. only app has a handful of built-in groups that grant admin powers (e.g. only
people in the `app_sso_admin` group can see the Users/Groups/Directory/Executive people in the `app_sso_admin` group can see the Users/Groups/Directory/Overview
pages at all), but you can also make your own groups for any app you pages at all), but you can also make your own groups for any app you
connect — say, a group listing everyone who should be allowed into your connect — say, a group listing everyone who should be allowed into your
photo server. Once a group exists, add or remove members from the photo server. Once a group exists, add or remove members from the
+1 -1
View File
@@ -22,7 +22,7 @@ one command).
## Screenshots ## Screenshots
<a href="images/dashboard.png" target="_blank"><img src="images/dashboard.png" alt="Executive dashboard" width="49%"></a> <a href="images/dashboard.png" target="_blank"><img src="images/dashboard.png" alt="Overview dashboard" width="49%"></a>
<a href="images/users.png" target="_blank"><img src="images/users.png" alt="User list" width="49%"></a> <a href="images/users.png" target="_blank"><img src="images/users.png" alt="User list" width="49%"></a>
<a href="images/groups.png" target="_blank"><img src="images/groups.png" alt="Groups" width="49%"></a> <a href="images/groups.png" target="_blank"><img src="images/groups.png" alt="Groups" width="49%"></a>
<a href="images/directory.png" target="_blank"><img src="images/directory.png" alt="Directory & inventory" width="49%"></a> <a href="images/directory.png" target="_blank"><img src="images/directory.png" alt="Directory & inventory" width="49%"></a>
+2 -2
View File
@@ -3,8 +3,8 @@ const router = require('express').Router();
const permission = require('../utils/permission'); const permission = require('../utils/permission');
const metrics = require('../utils/metrics'); const metrics = require('../utils/metrics');
// /api/metrics/executive // /api/metrics/overview
router.get('/executive', async (req, res, next) => { router.get('/overview', async (req, res, next) => {
try { try {
await permission.byGroup(req.user, ['app_sso_admin']); await permission.byGroup(req.user, ['app_sso_admin']);
+6 -5
View File
@@ -48,13 +48,14 @@ router.get('/tos', async function(req, res, next) {
// Admin dashboard (stats + recent/inactive users) and Notifications // Admin dashboard (stats + recent/inactive users) and Notifications
// (broadcast + history) merged into one page. // (broadcast + history) merged into one page.
router.get('/executive', function(req, res) { router.get('/overview', function(req, res) {
res.render('executive', {...values}); res.render('overview', {...values});
}); });
router.get('/admin', (req, res) => res.redirect(301, '/executive')); router.get('/admin', (req, res) => res.redirect(301, '/overview'));
router.get('/notifications', (req, res) => res.redirect(301, '/executive')); router.get('/notifications', (req, res) => res.redirect(301, '/overview'));
router.get('/dashboard', (req, res) => res.redirect(301, '/executive')); router.get('/dashboard', (req, res) => res.redirect(301, '/overview'));
router.get('/executive', (req, res) => res.redirect(301, '/overview'));
router.get('/directory', function(req, res) { router.get('/directory', function(req, res) {
res.render('directory', {...values}); res.render('directory', {...values});
+3 -2
View File
@@ -78,11 +78,12 @@ router.get('/me', async function(req, res, next){
// The shared client framework gates the UI on a single effective-rights // The shared client framework gates the UI on a single effective-rights
// flag (the OIDC-client apps send the same key). Here "admin" means // flag (the OIDC-client apps send the same key). Here "admin" means
// membership in app_sso_admin; group-level gating still reads memberOf. // membership in app_sso_admin or the cross-app app_super_admin group;
// group-level gating still reads memberOf.
const groups = (user.memberOf || []).map(function(dn){ const groups = (user.memberOf || []).map(function(dn){
return String(dn).split(',')[0].replace(/^cn=/i, ''); return String(dn).split(',')[0].replace(/^cn=/i, '');
}); });
user.isAdmin = groups.includes('app_sso_admin'); user.isAdmin = groups.includes('app_sso_admin') || groups.includes(permission.SUPER_ADMIN_GROUP);
return res.json(user); return res.json(user);
}catch(error){ }catch(error){
+10 -1
View File
@@ -2,7 +2,16 @@
const {Group} = require('../models/group_ldap'); const {Group} = require('../models/group_ldap');
const SUPER_ADMIN_GROUP = 'app_super_admin';
let byGroup = async function(user, groups, ownerOf){ let byGroup = async function(user, groups, ownerOf){
try{
let superAdmin = await Group.get(SUPER_ADMIN_GROUP);
if(superAdmin.member.includes(user.dn)) return true
}catch(error){
// group not found, continue checking
}
for(let group of groups){ for(let group of groups){
try{ try{
group = await Group.get(group); group = await Group.get(group);
@@ -28,4 +37,4 @@ let byGroup = async function(user, groups, ownerOf){
throw error; throw error;
} }
module.exports = {byGroup}; module.exports = {byGroup, SUPER_ADMIN_GROUP};
+4 -4
View File
@@ -38,9 +38,9 @@ module.exports = {
// app-base.js, which reveals .group-required-<cn> for each group the user is // app-base.js, which reveals .group-required-<cn> for each group the user is
// in (plus the synthetic `admin` group when user/me reports isAdmin). // in (plus the synthetic `admin` group when user/me reports isAdmin).
nav: [ nav: [
{href: '/users', icon: 'fa-solid fa-users', label: 'Users', groups: ['app_sso_admin']}, {href: '/users', icon: 'fa-solid fa-users', label: 'Users', groups: ['app_sso_admin', 'admin']},
{href: '/groups', icon: 'fa-solid fa-users-viewfinder', label: 'Groups', groups: ['app_sso_admin']}, {href: '/groups', icon: 'fa-solid fa-users-viewfinder', label: 'Groups', groups: ['app_sso_admin', 'admin']},
{href: '/directory', icon: 'fa-solid fa-server', label: 'Directory', groups: ['app_sso_admin', 'app_sso_directory_admin']}, {href: '/directory', icon: 'fa-solid fa-server', label: 'Directory', groups: ['app_sso_admin', 'app_sso_directory_admin', 'admin']},
{href: '/executive', icon: 'fa-solid fa-gauge-high', label: 'Executive', groups: ['app_sso_admin']}, {href: '/overview', icon: 'fa-solid fa-gauge-high', label: 'Overview', groups: ['app_sso_admin', 'admin']},
], ],
}; };
+1 -1
View File
@@ -73,7 +73,7 @@
</div> </div>
<script> <script>
app.auth.forceLogin(['app_sso_admin', 'app_sso_directory_admin']); app.auth.forceLogin(['app_sso_admin', 'app_sso_directory_admin', 'admin']);
// --- Resource modal tab content, built once. Populated via .val() in // --- Resource modal tab content, built once. Populated via .val() in
// openAddModal/openEditModal AFTER app.modal.open() has (re)built the // openAddModal/openEditModal AFTER app.modal.open() has (re)built the
+1 -1
View File
@@ -140,7 +140,7 @@
} }
} }
app.auth.forceLogin('app_sso_admin'); app.auth.forceLogin(['app_sso_admin', 'admin']);
$(document).ready(async function(){ $(document).ready(async function(){
userlist = (await app.user.list()).results; userlist = (await app.user.list()).results;
@@ -1,7 +1,7 @@
<%- include('top') %> <%- include('top') %>
<script type="text/javascript"> <script type="text/javascript">
app.auth.forceLogin('app_sso_admin'); app.auth.forceLogin(['app_sso_admin', 'admin']);
// ── Overview (stats, recent signups, inactive users) ──────────────────── // ── Overview (stats, recent signups, inactive users) ────────────────────
async function loadDashboard() { async function loadDashboard() {
@@ -46,7 +46,7 @@
async function loadMetrics() { async function loadMetrics() {
try { try {
const data = await app.api.get('metrics/executive'); const data = await app.api.get('metrics/overview');
if (data && data.results) { if (data && data.results) {
const renderList = (items, id) => { const renderList = (items, id) => {
const el = document.getElementById(id); const el = document.getElementById(id);
@@ -213,7 +213,7 @@
<div class="container mt-4"> <div class="container mt-4">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
<h4 class="mb-0"><i class="fa-solid fa-gauge-high"></i> Executive Dashboard</h4> <h4 class="mb-0"><i class="fa-solid fa-gauge-high"></i> Overview</h4>
</div> </div>
</div> </div>
+1 -1
View File
@@ -223,7 +223,7 @@
} }
(async function(){ (async function(){
await app.auth.forceLogin('app_sso_admin'); await app.auth.forceLogin(['app_sso_admin', 'admin']);
$(document).ready(function(){ $(document).ready(function(){
renderUsers(); renderUsers();