e9b808d1c2
- app_super_admin is a new cross-app LDAP group (also recognized by proxy and jump-host) that grants full admin here regardless of app_sso_admin membership: bypassed centrally in utils/permission.js's byGroup, folded into GET /api/user/me's isAdmin flag, and added to nav/forceLogin gates alongside app_sso_admin. - Renamed the Executive page to Overview (route, view, API path /api/metrics/overview, nav label, docs), keeping /executive as a 301 redirect alongside the existing /admin, /notifications, /dashboard legacy redirects.
303 lines
12 KiB
Plaintext
303 lines
12 KiB
Plaintext
<%- include('top') %>
|
|
|
|
<script type="text/javascript">
|
|
var userlist;
|
|
var allGroups = [];
|
|
|
|
function processGroup(value){
|
|
if (!Array.isArray(value.member)) value.member = value.member ? [value.member] : [];
|
|
if (!Array.isArray(value.owner)) value.owner = value.owner ? [value.owner] : [];
|
|
value.toAdd = userlist.filter(function(user){
|
|
return !value.member.includes(user.dn);
|
|
});
|
|
value.toAddOwner = userlist.filter(function(user){
|
|
return !value.owner.includes(user.dn);
|
|
});
|
|
value.member = value.member.map(function(user){
|
|
return {
|
|
dn: user,
|
|
uid: user.match(/cn=[a-zA-Z0-9\_\-\@\.]+/)[0].replace('cn=', '')
|
|
};
|
|
});
|
|
value.owner = value.owner.map(function(user){
|
|
return {
|
|
dn: user,
|
|
uid: user.match(/cn=[a-zA-Z0-9\_\-\@\.]+/)[0].replace('cn=', '')
|
|
};
|
|
});
|
|
value.memberCount = value.member.length;
|
|
value.createTimestamp = moment(value.createTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
|
value.modifyTimestamp = moment(value.modifyTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
|
value.groupCN = value.cn;
|
|
return value;
|
|
}
|
|
|
|
// app_sso_service_account is a marker group: membership hides an account
|
|
// from the Users page's People tab entirely (see users.ejs), which is
|
|
// exactly right for a non-person account but has silently made a real
|
|
// person's account look "gone" before (nothing else about it changes).
|
|
// Everywhere else in this dropdown just fires the PUT directly; only
|
|
// this one group gets a confirmation first.
|
|
function addMemberClick(event, groupCN, uid, el){
|
|
event.preventDefault();
|
|
const $el = $(el);
|
|
(async function(){
|
|
if (groupCN === 'app_sso_service_account') {
|
|
const ok = await app.messages.confirm(
|
|
`Mark "${uid}" as a service account? This hides them from the Users page's People tab (Service Accounts tab only) — only do this for a non-person account.`,
|
|
$el.closest('.card'), 'warning'
|
|
);
|
|
if (!ok) return;
|
|
}
|
|
try {
|
|
const data = await app.api.put(`group/${groupCN}/${uid}`, {});
|
|
await addedUser(data.message, groupCN, uid, $el);
|
|
} catch(e) {
|
|
app.messages.action(e.message || 'Failed to add member', $el.closest('.card'), 'danger');
|
|
}
|
|
})();
|
|
return false;
|
|
}
|
|
|
|
async function addedUser(message, group, user, $form){
|
|
let data = await app.group.get(group);
|
|
$.scope.groupCard.update('cn', group, processGroup(data.results));
|
|
app.messages.action(message, $("#group-card-"+group), 'success');
|
|
$('a[href="#'+$form.closest('.tab-pane').attr('id')+'"]').tab('show');
|
|
setTimeout(function(){ app.util.revealItem($("#group-card-" + group)); }, 400);
|
|
}
|
|
|
|
function applySort() {
|
|
const sort = $('#groupSort').val();
|
|
const scope = $.scope.groupCard;
|
|
if (sort === 'name-asc') { scope.__jqOrderBy = 'cn'; scope.__jqOrderReverse = false; }
|
|
if (sort === 'name-desc') { scope.__jqOrderBy = 'cn'; scope.__jqOrderReverse = true; }
|
|
if (sort === 'members-desc') { scope.__jqOrderBy = 'memberCount'; scope.__jqOrderReverse = true; }
|
|
if (sort === 'members-asc') { scope.__jqOrderBy = 'memberCount'; scope.__jqOrderReverse = false; }
|
|
}
|
|
|
|
function matchesSearch(g) {
|
|
const q = $('#groupSearch').val().toLowerCase().trim();
|
|
return !q || g.cn.toLowerCase().includes(q) || (g.description || '').toLowerCase().includes(q);
|
|
}
|
|
|
|
function applyFilters() {
|
|
applySort();
|
|
const groups = allGroups.filter(matchesSearch);
|
|
$.scope.groupCard.empty();
|
|
$.scope.groupCard.push(...groups);
|
|
$('#groupCount').text(groups.length + ' of ' + allGroups.length + ' group' + (allGroups.length !== 1 ? 's' : ''));
|
|
}
|
|
|
|
async function tableAJAX(revealCn) {
|
|
let data = await app.group.list();
|
|
allGroups = data.results.map(processGroup);
|
|
applyFilters();
|
|
if (revealCn) setTimeout(function(){ app.util.revealItem($('#group-card-' + revealCn)); }, 100);
|
|
}
|
|
|
|
async function removeMember(groupCN, uid, btn) {
|
|
const $item = $(btn).closest('li');
|
|
$item.addClass('list-group-item-warning');
|
|
const confirmed = await app.messages.confirm(`Remove "${uid}" from "${groupCN}"?`, $item, 'warning');
|
|
if (!confirmed) { $item.removeClass('list-group-item-warning'); return; }
|
|
try {
|
|
const data = await app.api.delete(`group/${groupCN}/${uid}`);
|
|
const groupData = await app.group.get(groupCN);
|
|
$.scope.groupCard.update('cn', groupCN, processGroup(groupData.results));
|
|
app.messages.action(data.message, $('#group-card-' + groupCN), 'success');
|
|
} catch(e) {
|
|
$item.removeClass('list-group-item-warning');
|
|
app.messages.action(e.message || 'Failed to remove member', $('#group-card-' + groupCN), 'danger');
|
|
}
|
|
}
|
|
|
|
async function removeOwner(groupCN, uid, btn) {
|
|
const $item = $(btn).closest('li');
|
|
$item.addClass('list-group-item-warning');
|
|
const confirmed = await app.messages.confirm(`Remove "${uid}" as owner of "${groupCN}"?`, $item, 'warning');
|
|
if (!confirmed) { $item.removeClass('list-group-item-warning'); return; }
|
|
try {
|
|
const data = await app.api.delete(`group/owner/${groupCN}/${uid}`);
|
|
const groupData = await app.group.get(groupCN);
|
|
$.scope.groupCard.update('cn', groupCN, processGroup(groupData.results));
|
|
app.messages.action(data.message, $('#group-card-' + groupCN), 'success');
|
|
} catch(e) {
|
|
$item.removeClass('list-group-item-warning');
|
|
app.messages.action(e.message || 'Failed to remove owner', $('#group-card-' + groupCN), 'danger');
|
|
}
|
|
}
|
|
|
|
async function deleteGroup(cn, btn) {
|
|
const $card = $(btn).closest('.card');
|
|
const confirmed = await app.messages.confirm(`Delete group "${cn}"?`, $card, 'danger');
|
|
if (!confirmed) return;
|
|
try {
|
|
await app.api.delete(`group/${cn}`);
|
|
$.scope.groupCard.remove('cn', cn);
|
|
} catch(e) {
|
|
app.messages.action(e.message || 'Failed to delete group', $card, 'danger');
|
|
}
|
|
}
|
|
|
|
app.auth.forceLogin(['app_sso_admin', 'admin']);
|
|
|
|
$(document).ready(async function(){
|
|
userlist = (await app.user.list()).results;
|
|
tableAJAX();
|
|
});
|
|
</script>
|
|
<div class="container mt-4">
|
|
|
|
<div class="d-flex flex-wrap gap-2 align-items-center sticky-top bg-body py-2" style="top: var(--sw-content-offset, 0);">
|
|
<div class="input-group" style="flex: 1 1 200px;">
|
|
<span class="input-group-text"><i class="fa-solid fa-magnifying-glass"></i></span>
|
|
<input type="text" id="groupSearch" class="form-control" placeholder="Search groups…" oninput="applyFilters()">
|
|
</div>
|
|
<select id="groupSort" class="form-select" style="width:auto; min-width:175px" onchange="applyFilters()">
|
|
<option value="name-asc">Name A → Z</option>
|
|
<option value="name-desc">Name Z → A</option>
|
|
<option value="members-desc">Most members</option>
|
|
<option value="members-asc">Fewest members</option>
|
|
</select>
|
|
<span id="groupCount" class="text-muted text-nowrap small"></span>
|
|
</div>
|
|
<div class="row row-cols-1 row-cols-md-3 g-4 mt-0">
|
|
<div class="col">
|
|
<div class="card shadow">
|
|
<div class="card-header">
|
|
<i class="fa-solid fa-object-group"></i>
|
|
Add new group
|
|
<a href="/docs/accounts" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
|
</div>
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
<div class="card-body">
|
|
<form action="group/" method="post" onsubmit="formAJAX(this)" evalAJAX="tableAJAX(data.results.cn)">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control shadow" name="name" placeholder="app_gitea_admin" validate=":3" />
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control shadow" name="description" placeholder="Admin group for gitea app" validate=":3"></textarea>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-outline-dark">Add</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col" jq-repeat="groupCard" jq-index-key="cn" jr-order-by="cn" id="group-card-{{cn}}">
|
|
<div class="card shadow col">
|
|
<div class="card-header">
|
|
<h5>
|
|
<i class="fa-solid fa-arrows-down-to-people"></i>
|
|
Group: {{ cn }}
|
|
<a href="/docs/accounts" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
|
</h5>
|
|
<ul class="nav nav-tabs card-header-tabs" id="myTab" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" id="group-members-tab-{{cn}}" data-bs-toggle="tab" data-bs-target="#group-memmbers-{{cn}}" href="#group-memmbers-{{cn}}" role="tab" aria-controls="member" aria-selected="true">
|
|
<i class="fa-solid fa-users"></i>
|
|
Members
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="group-admins-tab-{{cn}}" data-bs-toggle="tab" data-bs-target="#group-admins-{{cn}}" href="#group-admins-{{cn}}" role="tab" aria-controls="admin" aria-selected="false">
|
|
<i class="fa-solid fa-user-tie"></i>
|
|
Owners
|
|
</a>
|
|
</li>
|
|
<li class="nav-item float-end">
|
|
|
|
</li>
|
|
</ul>
|
|
|
|
</div>
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
<div class="card-body">
|
|
<p>
|
|
{{ description }}
|
|
</p>
|
|
<div class="tab-content" id="myTabContent">
|
|
<div class="tab-pane fade show active" id="group-memmbers-{{cn}}" role="tabpanel" aria-labelledby="member-tab">
|
|
<p>
|
|
<ul class="list-group">
|
|
{{ #member }}
|
|
<li id="group-card-{{cn}}-{{uid}}" class="list-group-item shadow">
|
|
<i class="fa-solid fa-user"></i> {{ uid }}
|
|
<button type="button" onclick="removeMember('{{groupCN}}', '{{uid}}', this)" class="btn btn-sm btn-danger float-end">
|
|
<i class="fa-solid fa-user-slash"></i>
|
|
</button>
|
|
</li>
|
|
{{ /member }}
|
|
</ul>
|
|
</p>
|
|
<div class="dropdown">
|
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="group_add_member" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
<i class="fa-solid fa-user-plus"></i>
|
|
</button>
|
|
<div class="dropdown-menu shadow-lg" aria-labelledby="group_add_member">
|
|
{{ #toAdd }}{{#.}}
|
|
<a class="dropdown-item" href="#" onclick="return addMemberClick(event, '{{groupCN}}', '{{uid}}', this);">
|
|
<i class="fa-solid fa-user"></i> {{uid}}
|
|
</a>
|
|
{{/.}}{{ /toAdd }}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="group-admins-{{cn}}" role="tabpanel" aria-labelledby="admin-tab">
|
|
<p>
|
|
<ul class="list-group">
|
|
{{ #owner }}
|
|
<li class="list-group-item shadow">
|
|
<i class="fa-solid fa-user"></i> {{ uid }}
|
|
<button type="button" onclick="removeOwner('{{groupCN}}', '{{uid}}', this)" class="btn btn-sm btn-danger float-end">
|
|
<i class="fa-solid fa-user-slash"></i>
|
|
</button>
|
|
</li>
|
|
{{ /owner }}
|
|
</ul>
|
|
</p>
|
|
|
|
<div class="dropdown float-start">
|
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="group_add_admin" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
<i class="fa-solid fa-user-plus"></i>
|
|
</button>
|
|
<div class="dropdown-menu shadow-lg" aria-labelledby="group_add_admin">
|
|
{{ #toAddOwner }}{{#.}}
|
|
<a class="dropdown-item" action="group/owner/{{groupCN}}/{{uid}}" method="put" onclick="formAJAX(this)" evalAJAX="addedUser(data.message, '{{groupCN}}', '{{uid}}', $form)">
|
|
<i class="fa-solid fa-user"></i> {{uid}}
|
|
</a>
|
|
{{/.}}{{ /toAddOwner }}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div class="float-end">
|
|
<button type="button" onclick="" class="btn btn-warning btn-lg shadow">
|
|
<i class="fa-solid fa-edit"></i>
|
|
</button>
|
|
<button type="button" onclick="deleteGroup('{{cn}}', this)" class="btn btn-danger btn-lg">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
<div>
|
|
Created: {{createTimestamp}}<br />
|
|
Last Modified: {{modifyTimestamp}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<%- include('bottom') %>
|