feat: actionable metrics, LDAP log parsing, UI updates
This commit is contained in:
+373
-174
@@ -23,6 +23,132 @@
|
||||
renderUsers();
|
||||
});
|
||||
}
|
||||
|
||||
function inviteStatus(t) {
|
||||
if (t.claimed_by && t.claimed_by !== '__NONE__') return { label: 'Claimed', cls: 'text-secondary' };
|
||||
if (!t.is_valid) return { label: 'Revoked', cls: 'text-danger' };
|
||||
return { label: 'Pending', cls: 'text-success' };
|
||||
}
|
||||
|
||||
function groupsDisplay(groups) {
|
||||
try {
|
||||
const arr = JSON.parse(groups || '[]');
|
||||
return arr.length ? arr.join(', ') : '—';
|
||||
} catch(_) { return '—'; }
|
||||
}
|
||||
|
||||
async function loadInvites() {
|
||||
try {
|
||||
const data = await app.api.get('user/invite');
|
||||
const tbody = document.getElementById('invite-tbody');
|
||||
tbody.innerHTML = '';
|
||||
(data.results || [])
|
||||
.sort((a, b) => (b.created_on || 0) - (a.created_on || 0))
|
||||
.forEach(function(t) {
|
||||
const status = inviteStatus(t);
|
||||
const canEdit = true; // all users on this page are admins
|
||||
const mail = (!t.mail || t.mail === '__NONE__') ? '—' : t.mail;
|
||||
const isPending = t.is_valid && (!t.claimed_by || t.claimed_by === '__NONE__');
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.token = t.token;
|
||||
tr.innerHTML = `
|
||||
<td><small class="text-muted">${moment(Number(t.created_on)).fromNow()}</small></td>
|
||||
<td><small>${t.created_by}</small></td>
|
||||
<td><small>${mail}</small></td>
|
||||
<td><small class="text-muted">${groupsDisplay(t.groups)}</small></td>
|
||||
<td><small class="${status.cls}">${status.label}</small></td>
|
||||
<td class="text-nowrap text-end">
|
||||
${canEdit && isPending ? `
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" onclick="openEdit('${t.token}')">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="revokeInvite('${t.token}', this)">
|
||||
<i class="fa-solid fa-ban"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
${isPending ? `
|
||||
<button class="btn btn-sm btn-outline-dark ms-1" onclick="copyLink('${t.token}', this)">
|
||||
<i class="fa-solid fa-copy"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch(e) {
|
||||
if (e && e.status === 401) location.replace('/');
|
||||
else console.error('Failed to load invites:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function populateGroupSelect(id, groups, selected) {
|
||||
const sel = document.getElementById(id);
|
||||
sel.innerHTML = '';
|
||||
groups.forEach(function(cn) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = cn;
|
||||
opt.textContent = cn;
|
||||
opt.selected = selected.includes(cn);
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function openEdit(tokenId) {
|
||||
app.api.get('user/invite').then(function(data) {
|
||||
const t = (data.results || []).find(x => x.token === tokenId);
|
||||
if (!t) return;
|
||||
document.getElementById('edit-token').value = tokenId;
|
||||
document.getElementById('edit-email').value = (t.mail && t.mail !== '__NONE__') ? t.mail : '';
|
||||
const selected = JSON.parse(t.groups || '[]');
|
||||
const allGroups = [...document.getElementById('invite-groups').options].map(o => o.value);
|
||||
populateGroupSelect('edit-groups', allGroups, selected);
|
||||
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
const tokenId = document.getElementById('edit-token').value;
|
||||
const mail = document.getElementById('edit-email').value.trim();
|
||||
const groups = [...document.getElementById('edit-groups').selectedOptions].map(o => o.value);
|
||||
const result = document.getElementById('edit-result');
|
||||
result.style.display = 'none';
|
||||
try {
|
||||
await app.api.put(`user/invite/${tokenId}`, { mail, groups });
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
loadInvites();
|
||||
} catch(e) {
|
||||
result.style.display = '';
|
||||
result.className = 'alert alert-danger mt-2';
|
||||
result.textContent = 'Failed: ' + ((e.responseJSON && e.responseJSON.message) || 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeInvite(tokenId, btn) {
|
||||
$thisRow = $(btn).closest('tr');
|
||||
$thisRow.addClass('table-warning');
|
||||
let confirmation = await app.util.actionConfirm('Revoke selected invite token?', $thisRow, 'warning');
|
||||
if(!confirmation){
|
||||
$thisRow.removeClass('table-warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await app.api.delete(`user/invite/${tokenId}`);
|
||||
loadInvites();
|
||||
} catch(e) {
|
||||
alert('Failed to revoke invite.');
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink(tokenId, btn) {
|
||||
const link = `${location.origin}/login/invite/${tokenId}`;
|
||||
navigator.clipboard.writeText(link).then(function() {
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
||||
setTimeout(() => btn.innerHTML = orig, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteUser(uid, btn){
|
||||
const $row = $(btn).closest('tr');
|
||||
@@ -84,6 +210,11 @@
|
||||
}
|
||||
document.getElementById('invite-email').value = '';
|
||||
[...document.getElementById('invite-groups').options].forEach(o => o.selected = false);
|
||||
loadInvites();
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById("inviteUserModal"))?.hide();
|
||||
result.style.display = 'none';
|
||||
}, 3000);
|
||||
}catch(e){
|
||||
result.style.display = '';
|
||||
result.className = 'alert alert-danger mt-2';
|
||||
@@ -97,199 +228,267 @@
|
||||
$(document).ready(function(){
|
||||
renderUsers();
|
||||
loadInviteGroups();
|
||||
$('form[action="user/"]').attr('evalAJAX', 'renderUsers("User added", "success")')
|
||||
loadInvites();
|
||||
$('form[action="user/"]').attr('evalAJAX', 'renderUsers("User added", "success"); bootstrap.Modal.getInstance(document.getElementById("addUserModal"))?.hide();')
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
||||
<h4><i class="fa-solid fa-users"></i> Users</h4>
|
||||
<div class="container mt-4">
|
||||
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-people-btn" data-bs-toggle="tab" data-bs-target="#tab-people" type="button" role="tab">
|
||||
<i class="fa-solid fa-user"></i> People
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-service-accounts-btn" data-bs-toggle="tab" data-bs-target="#tab-service-accounts" type="button" role="tab">
|
||||
<i class="fa-solid fa-gears"></i> Service Accounts
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tab-people" role="tabpanel">
|
||||
<div class="row" style="display:none">
|
||||
<div class="col-md-4">
|
||||
<div class="shadow-lg card mb-3 card-default group-required group-required-app_sso_admin">
|
||||
<div class="card-header shadow">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
Invite User
|
||||
<span class="float-end">
|
||||
<a href="/docs/accounts" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-header shadow actionMessage" style="display: none;"></div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Email <small class="text-muted">(optional — sends invite immediately)</small></label>
|
||||
<input type="email" id="invite-email" class="form-control form-control-sm shadow" placeholder="user@example.com" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Groups <small class="text-muted">(optional — hold Ctrl/⌘ for multiple)</small></label>
|
||||
<input type="text" class="form-control form-control-sm shadow mb-1" placeholder="Filter groups…" oninput="filterGroups(this, 'invite-groups')" />
|
||||
<select id="invite-groups" class="form-select form-select-sm shadow" multiple size="4"></select>
|
||||
</div>
|
||||
<button onclick="sendInvite()" class="btn btn-sm btn-outline-dark shadow">
|
||||
<i class="fa-solid fa-envelope"></i> Send Invite
|
||||
</button>
|
||||
<div id="invite-result" style="display:none" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
Add new user
|
||||
<small class="text-muted">(check <b>This is a service account</b> below to create one — it'll show up under the Service Accounts tab)</small>
|
||||
<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">
|
||||
<%- include('user_form', {adminMode: true}) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
User List
|
||||
<a href="/docs/accounts" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fa-solid fa-users"></i>
|
||||
User List
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary me-2 group-required group-required-app_sso_admin" data-bs-toggle="modal" data-bs-target="#inviteUserModal">
|
||||
<i class="fas fa-envelope"></i> Invite User
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary me-2" data-bs-toggle="modal" data-bs-target="#addUserModal">
|
||||
<i class="fas fa-user-plus"></i> Add User
|
||||
</button>
|
||||
<a href="/docs/accounts" class="text-reset" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header actionMessage" style="display:none"></div>
|
||||
<div class="table-responsive">
|
||||
<table class="card-body table table-striped" style="margin-bottom:0">
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>eMail</th>
|
||||
<th>Key</th>
|
||||
<th>Active</th>
|
||||
<th>TOS</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody id="tableAJAX">
|
||||
<tr jq-repeat="userRow">
|
||||
<td>
|
||||
{{ uidNumber }}
|
||||
</td>
|
||||
<td>
|
||||
<a href='/users/{{uid}}'>{{givenName}} {{sn}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{mail}}
|
||||
</td>
|
||||
<td>
|
||||
{{#sshPublicKey}}<i class="fa-regular fa-circle-check text-success"></i>{{/sshPublicKey}}
|
||||
</td>
|
||||
<td>
|
||||
{{#isActive}}<i class="fa-regular fa-circle-check text-success"></i>{{/isActive}}
|
||||
{{#isInactive}}<i class="fa-solid fa-circle-xmark text-danger"></i>{{/isInactive}}
|
||||
</td>
|
||||
<td>
|
||||
{{#tosAccepted}}<i class="fa-solid fa-circle-check text-success" title="TOS accepted"></i>{{/tosAccepted}}
|
||||
{{#tosNotAccepted}}<i class="fa-solid fa-circle-xmark text-danger" title="TOS not accepted"></i>{{/tosNotAccepted}}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{#isActive}}
|
||||
<button class="btn btn-sm btn-outline-warning me-1" title="Deactivate" onclick="toggleActive('{{uid}}', false)">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</button>
|
||||
{{/isActive}}
|
||||
{{#isInactive}}
|
||||
<button class="btn btn-sm btn-warning me-1" title="Activate" onclick="toggleActive('{{uid}}', true)">
|
||||
<i class="fa-solid fa-lock-open"></i>
|
||||
</button>
|
||||
{{/isInactive}}
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" title="Impersonate" onclick="startImpersonate('{{uid}}')">
|
||||
<i class="fa-solid fa-user-secret"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('{{uid}}', this)">
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="px-3 pt-3 border-bottom">
|
||||
<ul class="nav nav-tabs border-bottom-0" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-people-btn" data-bs-toggle="tab" data-bs-target="#tab-people" type="button" role="tab">
|
||||
<i class="fa-solid fa-user"></i> People
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-service-accounts-btn" data-bs-toggle="tab" data-bs-target="#tab-service-accounts" type="button" role="tab">
|
||||
<i class="fa-solid fa-gears"></i> Service Accounts
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-invites-btn" data-bs-toggle="tab" data-bs-target="#tab-invites" type="button" role="tab">
|
||||
<i class="fa-solid fa-envelope-open-text"></i> Invites
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tab-people" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="card-body table table-striped" style="margin-bottom:0">
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>eMail</th>
|
||||
<th>Key</th>
|
||||
<th>Active</th>
|
||||
<th>TOS</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody id="tableAJAX">
|
||||
<tr jq-repeat="userRow">
|
||||
<td>
|
||||
{{ uidNumber }}
|
||||
</td>
|
||||
<td>
|
||||
<a href='/users/{{uid}}'>{{givenName}} {{sn}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{mail}}
|
||||
</td>
|
||||
<td>
|
||||
{{#sshPublicKey}}<i class="fa-regular fa-circle-check text-success"></i>{{/sshPublicKey}}
|
||||
</td>
|
||||
<td>
|
||||
{{#isActive}}<i class="fa-regular fa-circle-check text-success"></i>{{/isActive}}
|
||||
{{#isInactive}}<i class="fa-solid fa-circle-xmark text-danger"></i>{{/isInactive}}
|
||||
</td>
|
||||
<td>
|
||||
{{#tosAccepted}}<i class="fa-solid fa-circle-check text-success" title="TOS accepted"></i>{{/tosAccepted}}
|
||||
{{#tosNotAccepted}}<i class="fa-solid fa-circle-xmark text-danger" title="TOS not accepted"></i>{{/tosNotAccepted}}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{#isActive}}
|
||||
<button class="btn btn-sm btn-outline-warning me-1" title="Deactivate" onclick="toggleActive('{{uid}}', false)">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</button>
|
||||
{{/isActive}}
|
||||
{{#isInactive}}
|
||||
<button class="btn btn-sm btn-warning me-1" title="Activate" onclick="toggleActive('{{uid}}', true)">
|
||||
<i class="fa-solid fa-lock-open"></i>
|
||||
</button>
|
||||
{{/isInactive}}
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" title="Impersonate" onclick="startImpersonate('{{uid}}')">
|
||||
<i class="fa-solid fa-user-secret"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('{{uid}}', this)">
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-service-accounts" role="tabpanel">
|
||||
<div class="p-3 pb-0 text-muted small border-bottom">
|
||||
<i class="fa-solid fa-circle-info"></i> Unix/POSIX accounts something runs as, not a person. Create one from the People tab's "Add User" form.
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="card-body table table-striped" style="margin-bottom:0">
|
||||
<thead>
|
||||
<th>Username</th>
|
||||
<th>Description</th>
|
||||
<th>Manager(s)</th>
|
||||
<th>Created</th>
|
||||
<th>Active</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr jq-repeat="serviceAccountRow">
|
||||
<td>
|
||||
<a href='/users/{{uid}}'>{{uid}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{description}}
|
||||
</td>
|
||||
<td>
|
||||
{{#managerUids}}<span class="badge bg-secondary me-1">{{.}}</span>{{/managerUids}}
|
||||
</td>
|
||||
<td>
|
||||
{{createTimestamp}}
|
||||
</td>
|
||||
<td>
|
||||
{{#isActive}}<i class="fa-regular fa-circle-check text-success"></i>{{/isActive}}
|
||||
{{#isInactive}}<i class="fa-solid fa-circle-xmark text-danger"></i>{{/isInactive}}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{#isActive}}
|
||||
<button class="btn btn-sm btn-outline-warning me-1" title="Deactivate" onclick="toggleActive('{{uid}}', false)">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</button>
|
||||
{{/isActive}}
|
||||
{{#isInactive}}
|
||||
<button class="btn btn-sm btn-warning me-1" title="Activate" onclick="toggleActive('{{uid}}', true)">
|
||||
<i class="fa-solid fa-lock-open"></i>
|
||||
</button>
|
||||
{{/isInactive}}
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('{{uid}}', this)">
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tab-invites" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="card-body table table-striped" style="margin-bottom:0">
|
||||
<thead>
|
||||
<th>Created</th>
|
||||
<th>By</th>
|
||||
<th>Email</th>
|
||||
<th>Groups</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody id="invite-tbody">
|
||||
<tr><td colspan="6" class="text-center text-muted py-3">Loading…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-service-accounts" role="tabpanel">
|
||||
<div class="row" style="display:none">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<i class="fa-solid fa-gears"></i>
|
||||
Service Accounts
|
||||
<small class="text-muted">— Unix/POSIX accounts something runs as, not a person. Create one from the People tab's "Add new user" form.</small>
|
||||
<a href="/docs/accounts" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
||||
|
||||
<!-- Invite User Modal -->
|
||||
<div class="modal fade" id="inviteUserModal" tabindex="-1" aria-labelledby="inviteUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header shadow">
|
||||
<h5 class="modal-title" id="inviteUserModalLabel"><i class="fas fa-user-plus"></i> Invite User</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Email <small class="text-muted">(optional — sends invite immediately)</small></label>
|
||||
<input type="email" id="invite-email" class="form-control form-control-sm shadow" placeholder="user@example.com" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Groups <small class="text-muted">(optional — hold Ctrl/⌘ for multiple)</small></label>
|
||||
<input type="text" class="form-control form-control-sm shadow mb-1" placeholder="Filter groups…" oninput="filterGroups(this, 'invite-groups')" />
|
||||
<select id="invite-groups" class="form-select form-select-sm shadow" multiple size="4"></select>
|
||||
</div>
|
||||
<div id="invite-result" style="display:none" class="mt-2"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" onclick="sendInvite()" class="btn btn-primary shadow">
|
||||
<i class="fa-solid fa-envelope"></i> Send Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header shadow">
|
||||
<h5 class="modal-title" id="addUserModalLabel">
|
||||
<i class="fas fa-user-plus"></i> Add new user
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted mb-3">Check <b>This is a service account</b> below to create one — it'll show up under the Service Accounts tab.</p>
|
||||
<%- include('user_form', {adminMode: true}) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Invite Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header shadow">
|
||||
<h5 class="modal-title"><i class="fa-solid fa-pen"></i> Edit Invite</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit-token" />
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email <small class="text-muted">(leave blank to clear; changing sends a new verification email)</small></label>
|
||||
<input type="email" id="edit-email" class="form-control shadow" placeholder="user@example.com" />
|
||||
</div>
|
||||
<div class="card-header actionMessage" style="display:none"></div>
|
||||
<div class="table-responsive">
|
||||
<table class="card-body table table-striped" style="margin-bottom:0">
|
||||
<thead>
|
||||
<th>Username</th>
|
||||
<th>Description</th>
|
||||
<th>Manager(s)</th>
|
||||
<th>Created</th>
|
||||
<th>Active</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr jq-repeat="serviceAccountRow">
|
||||
<td>
|
||||
<a href='/users/{{uid}}'>{{uid}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{description}}
|
||||
</td>
|
||||
<td>
|
||||
{{#managerUids}}<span class="badge bg-secondary me-1">{{.}}</span>{{/managerUids}}
|
||||
</td>
|
||||
<td>
|
||||
{{createTimestamp}}
|
||||
</td>
|
||||
<td>
|
||||
{{#isActive}}<i class="fa-regular fa-circle-check text-success"></i>{{/isActive}}
|
||||
{{#isInactive}}<i class="fa-solid fa-circle-xmark text-danger"></i>{{/isInactive}}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{#isActive}}
|
||||
<button class="btn btn-sm btn-outline-warning me-1" title="Deactivate" onclick="toggleActive('{{uid}}', false)">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</button>
|
||||
{{/isActive}}
|
||||
{{#isInactive}}
|
||||
<button class="btn btn-sm btn-warning me-1" title="Activate" onclick="toggleActive('{{uid}}', true)">
|
||||
<i class="fa-solid fa-lock-open"></i>
|
||||
</button>
|
||||
{{/isInactive}}
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('{{uid}}', this)">
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Groups <small class="text-muted">(Ctrl/⌘ for multiple)</small></label>
|
||||
<input type="text" class="form-control form-control-sm shadow mb-1" placeholder="Filter groups…" oninput="filterGroups(this, 'edit-groups')" />
|
||||
<select id="edit-groups" class="form-select shadow" multiple size="6"></select>
|
||||
</div>
|
||||
<div id="edit-result" style="display:none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveEdit()">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('impersonate_modal') %>
|
||||
<%- include('bottom') %>
|
||||
|
||||
Reference in New Issue
Block a user