cdc5d1528c
Removes the LDAP bind-only service account type in favor of a single Unix/POSIX account model, surfaced in a new Users > Service Accounts tab. Adds a multi-valued `manager` field to every account (defaults to the creator, editable, and grants edit rights on the accounts a person manages without needing app_sso_admin). homeDirectory and loginShell are now editable from the profile edit form. Bumps to v1.1.7. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
537 lines
20 KiB
Plaintext
537 lines
20 KiB
Plaintext
<%- include('top') %>
|
|
|
|
<script type="text/javascript">
|
|
app.auth.forceLogin();
|
|
var currentUser;
|
|
var isOwnProfile = !location.pathname.includes('/users/');
|
|
|
|
function renderProfile(user){
|
|
// data.photo = unescape(encodeURIComponent(data.jpegPhoto));
|
|
user.createTimestamp = moment(user.createTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
|
user.modifyTimestamp = moment(user.modifyTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
|
user.managerUids = (user.manager || []).map(app.user.dnToUid);
|
|
|
|
$.scope.user.update(user);
|
|
$.scope.passwordReset.update(user);
|
|
};
|
|
|
|
async function renderUserGroups(user){
|
|
try{
|
|
let res = await app.api.get('group/?detail=true&member='+user.uid);
|
|
$.scope.mygroups.push(...res.results);
|
|
}catch(error){
|
|
console.error('renderUserGroups error:', error)
|
|
}
|
|
}
|
|
|
|
async function determinUser(){
|
|
if(location.pathname.includes('/users/')){
|
|
let uid = location.pathname.replace('/users/', '');
|
|
|
|
return (await app.api.get('user/'+uid)).results;
|
|
}else{
|
|
return await app.auth.asyncUser;
|
|
}
|
|
}
|
|
|
|
function editUser(user){
|
|
user = user || currentUser;
|
|
|
|
var $profileCard = $('#userProfile');
|
|
var $editCard = $('#editProfile');
|
|
|
|
$.scope.editProfile.update(user);
|
|
// jq-repeat's update() is trailing-edge throttled (~50ms) as of 2.1.0 --
|
|
// wait for the throttle tick to land before sliding the updated card
|
|
// into view, or it can briefly show stale/empty data. The manager
|
|
// picker is a JS widget, not a mustache-bound input, so it also has to
|
|
// wait for update() to (re-)render its empty mount div before attaching.
|
|
setTimeout(function(){
|
|
app.ui.userSelect('#edit-manager', {
|
|
name: 'manager',
|
|
values: user.managerUids || [],
|
|
placeholder: 'Type a username…',
|
|
});
|
|
$profileCard.slideUp();
|
|
$editCard.slideDown();
|
|
}, 60);
|
|
}
|
|
|
|
function editUserSeccess(data){
|
|
currentUser = data.results;
|
|
renderProfile(currentUser);
|
|
// Same throttle-tick wait as editUser() above -- renderProfile() calls
|
|
// $.scope.user.update()/passwordReset.update() internally.
|
|
setTimeout(function(){
|
|
$('#editProfile').slideUp();
|
|
$('#userProfile').slideDown()
|
|
}, 60);
|
|
}
|
|
|
|
async function toggleActive(uid, active){
|
|
app.user.setActive(uid, active, async function(error, data){
|
|
if(error) return alert('Failed to update user status');
|
|
currentUser = await determinUser();
|
|
renderProfile(currentUser);
|
|
});
|
|
}
|
|
|
|
async function deleteUser(uid, btn){
|
|
const $card = $(btn).closest('.card');
|
|
const confirmed = await app.util.actionConfirm(`Delete user "${uid}"?`, $card, 'warning');
|
|
if (!confirmed) return;
|
|
app.api.delete('user/' + uid, function(error, data){
|
|
if (error) {
|
|
app.util.actionMessage(data.message || 'Failed to delete user', $card, 'danger');
|
|
return;
|
|
}
|
|
window.location.href = '/users';
|
|
});
|
|
}
|
|
|
|
$(document).ready(async function(){
|
|
currentUser = await determinUser();
|
|
|
|
renderProfile(currentUser);
|
|
renderUserGroups(currentUser);
|
|
|
|
// API Tokens are self-service only — never shown when an admin is
|
|
// viewing someone else's profile via /users/:uid.
|
|
if(isOwnProfile){
|
|
$('#own-api-tokens-section').show();
|
|
tableAJAX();
|
|
$('form[action="api-token/"]').attr('evalAJAX',
|
|
'showSecret(data.token); tableAJAX(); $form.trigger("reset");'
|
|
);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div class="row mb-3" style="display:none">
|
|
<div class="col-md-4">
|
|
<div class="shadow-lg card">
|
|
<div class="card-header shadow">
|
|
<i class="fa-solid fa-arrow-rotate-left"></i>
|
|
Password Reset
|
|
<div class="float-end">
|
|
<i class="fa-solid fa-arrows-up-down"></i>
|
|
</div>
|
|
</div>
|
|
<div class="card-header shadow actionMessage" style="display:none">
|
|
</div>
|
|
<div jq-repeat="passwordReset" class="card-body">
|
|
<h3>
|
|
Reset Password for {{uid}}
|
|
</h3>
|
|
<form action="user/{{uid}}/password" method="put" onsubmit="formAJAX(this)" class="mb-3">
|
|
<div class="mb-3">
|
|
<label class="form-label">Password</label>
|
|
<div class="input-group shadow">
|
|
<span class="input-group-text" id="addon-wrapping"><i class="fa-solid fa-key"></i></span>
|
|
<input type="password" name="userPassword" class="form-control" placeholder="huunteR!23" aria-label="Username" aria-describedby="addon-wrapping">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Again</label>
|
|
<div class="input-group shadow">
|
|
<span class="input-group-text" id="addon-wrapping"><i class="fa-solid fa-key"></i></span>
|
|
<input type="password" name="password" class="form-control" placeholder="huunteR!23" aria-label="Username" aria-describedby="addon-wrapping">
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-outline-secondary shadow">Change</button>
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-8">
|
|
<div id="userProfile" class="shadow-lg card card-default mb-8">
|
|
<div class="card-header shadow">
|
|
<i class="fa-regular fa-id-card"></i>
|
|
Profile
|
|
<div class="float-end">
|
|
<i class="fa-solid fa-arrows-up-down"></i>
|
|
</div>
|
|
</div>
|
|
<div class="card-header shadow actionMessage" style="display:none">
|
|
</div>
|
|
<div class="profile-body" jq-repeat="user">
|
|
<div class="card-body profile-body-{{uid}}">
|
|
<h2><i>User Name:</i> <b>{{uid}}</b></h2>
|
|
<i>Name:</i> <b>{{givenName}} {{sn}}</b><br />
|
|
<i>Email:</i> <b>{{mail}}</b>
|
|
{{#emailVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/emailVerified}}
|
|
<br />
|
|
<i>Phone:</i> <b>{{mobile}}</b>
|
|
{{#phoneVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/phoneVerified}}
|
|
<br />
|
|
<i>LDAP DN:</i> <b>{{dn}} </b><br />
|
|
<i>Home Directory:</i> <b>{{homeDirectory}} </b><br />
|
|
<i>Login Shell:</i> <b>{{loginShell}} </b><br />
|
|
<i>Manager(s):</i>
|
|
{{#managerUids}}<span class="badge bg-secondary me-1">{{.}}</span>{{/managerUids}}
|
|
<br />
|
|
<i>Status:</i>
|
|
{{#isActive}}<span class="badge bg-success">Active</span>{{/isActive}}
|
|
{{#isInactive}}<span class="badge bg-danger">Inactive</span>{{/isInactive}}
|
|
<br />
|
|
<i>Date of Birth:</i> <b>{{dateOfBirth}}</b><br />
|
|
<i>TOS:</i>
|
|
{{#tosAccepted}}<span class="badge bg-success">Accepted</span>{{/tosAccepted}}
|
|
{{#tosNotAccepted}}<span class="badge bg-warning text-dark">Pending</span>{{/tosNotAccepted}}
|
|
<br />
|
|
<i>SSH Public Key:</i> <b>{{sshPublicKey}}</b><br />
|
|
<i>Unix User ID:</i> <b>{{uidNumber}} </b><br />
|
|
<i>Unix Group ID:</i> <b>{{gidNumber}} </b><br />
|
|
<i>Description:</i><br>
|
|
<p>
|
|
{{description}}
|
|
</p>
|
|
<!-- <img id="profile_photo" /> -->
|
|
</div>
|
|
<div class="card-footer profile-body-{{uid}}">
|
|
<div class="float-end">
|
|
<button type="button" onclick="editUser()" class="btn btn-warning btn shadow ">
|
|
<i class="fa-solid fa-user-pen"></i>
|
|
</button>
|
|
{{#isActive}}
|
|
<button type="button" class="btn btn-outline-warning shadow group-required group-required-app_sso_admin" title="Deactivate user" onclick="toggleActive('{{uid}}', false)">
|
|
<i class="fa-solid fa-lock"></i>
|
|
</button>
|
|
{{/isActive}}
|
|
{{#isInactive}}
|
|
<button type="button" class="btn btn-warning shadow group-required group-required-app_sso_admin" title="Activate user" onclick="toggleActive('{{uid}}', true)">
|
|
<i class="fa-solid fa-lock-open"></i>
|
|
</button>
|
|
{{/isInactive}}
|
|
<button type="button" class="btn btn-secondary shadow group-required group-required-app_sso_admin" title="Impersonate this user" onclick="startImpersonate('{{uid}}')">
|
|
<i class="fa-solid fa-user-secret"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-danger shadow group-required group-required-app_sso_admin" onclick="deleteUser('{{uid}}', this)">
|
|
<i class="fa-solid fa-user-slash"></i>
|
|
</button>
|
|
</div>
|
|
<div class="float-left">
|
|
<i>Joined:</i> <b>{{createTimestamp}} </b> <br/>
|
|
<i>Edited:</i> <b>{{modifyTimestamp}} </b>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="editProfile" class="shadow-lg card card-default mb-8" style="display:none">
|
|
<div class="card-header shadow">
|
|
<i class="fad fa-id-card"></i>
|
|
Edit Profile
|
|
<div class="float-end">
|
|
<i class="fa-solid fa-arrows-up-down"></i>
|
|
</div>
|
|
</div>
|
|
<div class="card-header shadow actionMessage" style="display:none">
|
|
</div>
|
|
<div class="card-body" jq-repeat="editProfile">
|
|
<div id="tableAJAX">
|
|
<h3>Editing {{uid}}</h3>
|
|
<form action="user/{{uid}}" method="put" onsubmit="formAJAX(this)" evalAJAX="editUserSeccess(data)">
|
|
<div class="mb-3">
|
|
<label class="form-label">Date of Birth</label>
|
|
<input type="date" class="form-control shadow" name="dateOfBirth" value="{{dateOfBirth}}" />
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">SSH Public Key</label>
|
|
<input type="text" class="form-control" name="sshPublicKey" placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQ..." value="{{sshPublicKey}}" />
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Mobile Phone</label>
|
|
<input type="text" class="form-control" name="mobile" placeholder="9175551234" validate=":9" value="{{mobile}}" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Home Directory</label>
|
|
<input type="text" class="form-control" name="homeDirectory" placeholder="/home/jsmith" value="{{homeDirectory}}" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Login Shell</label>
|
|
<input type="text" class="form-control" name="loginShell" placeholder="/bin/bash" value="{{loginShell}}" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Manager(s)</label>
|
|
<div id="edit-manager"></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">User Description (Optional)</label>
|
|
<textarea class="form-control" name="description" placeholder="Admin group for gitea app">{{description}}</textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-outline-dark btn-warning">Change</button>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="shadow-lg card card-default mb-8">
|
|
<div class="card-header shadow">
|
|
<i class="fa-solid fa-users-viewfinder"></i>
|
|
My groups
|
|
<div class="float-end">
|
|
<i class="fa-solid fa-arrows-up-down"></i>
|
|
</div>
|
|
</div>
|
|
<div class="card-header shadow actionMessage" style="display:none">
|
|
</div>
|
|
<div class="card-body" style="padding-bottom:0">
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<th>
|
|
Name
|
|
</th>
|
|
<th>
|
|
Description
|
|
</th>
|
|
</thead>
|
|
<tbody jq-repeat="mygroups">
|
|
<tr>
|
|
<td>{{cn}}</td>
|
|
<td>{{description}}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Token modal (shown once on create/rotate) -->
|
|
<div class="modal fade" id="secretModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fa-solid fa-key"></i> API Token</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-danger"><i class="fa-solid fa-triangle-exclamation"></i> Save this token now — it will <strong>not</strong> be shown again.</p>
|
|
<div class="input-group">
|
|
<input type="text" id="secretValue" class="form-control font-monospace" readonly>
|
|
<button class="btn btn-outline-secondary" onclick="copySecret()" title="Copy">
|
|
<i class="fa-solid fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
<p class="mt-3 mb-0 text-muted small">Use it as a bearer token:<br><code>Authorization: Bearer <token></code></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Done</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit modal -->
|
|
<div class="modal fade" id="editModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fa-solid fa-pen-to-square"></i> Edit API Token</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="card-header actionMessage mb-3" style="display:none"></div>
|
|
<input type="hidden" id="edit-id">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" id="edit-name" class="form-control shadow">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<input type="text" id="edit-description" class="form-control shadow">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Expires in (days) <small class="text-muted">(0 = never)</small></label>
|
|
<input type="number" id="edit-expires_in_days" class="form-control shadow" min="0">
|
|
</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(this)"><i class="fa-solid fa-floppy-disk"></i> Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
// Any logged-in user can manage their own API tokens (self-service).
|
|
// Section is only revealed (see $(document).ready above) when isOwnProfile.
|
|
|
|
var secretModal = new bootstrap.Modal(document.getElementById('secretModal'));
|
|
var editModal = new bootstrap.Modal(document.getElementById('editModal'));
|
|
var tokensById = {};
|
|
|
|
function showSecret(secret){
|
|
document.getElementById('secretValue').value = secret;
|
|
secretModal.show();
|
|
}
|
|
function copySecret(){ copyField('secretValue'); }
|
|
|
|
function copyField(id, btn){
|
|
var el = document.getElementById(id);
|
|
if(!el) return;
|
|
el.select(); el.setSelectionRange(0, 99999); document.execCommand('copy');
|
|
if(btn){ var $i = $(btn).find('i'), prev = $i.attr('class');
|
|
$i.attr('class', 'fa-solid fa-check'); setTimeout(function(){ $i.attr('class', prev); }, 1200); }
|
|
}
|
|
|
|
function fmtTime(ms){
|
|
// created_on/last_used_on come back from Redis as strings (model-redis
|
|
// only coerces fields with an explicit `type`); moment(value, "x") parses
|
|
// a numeric string-or-number as a Unix-ms timestamp, unlike new Date(str).
|
|
if(!ms || Number(ms) === 0) return '—';
|
|
var t = moment(ms, "x");
|
|
if(!t.isValid()) return '—';
|
|
return t.fromNow() + ' <span class="text-muted">(' + t.format('YYYY-MM-DD HH:mm') + ')</span>';
|
|
}
|
|
function fmtExpiry(token){
|
|
// expires_at is type:number (a real number); isExpired is a class getter
|
|
// that is NOT serialized to the client, so compute expiry here.
|
|
var exp = Number(token.expires_at);
|
|
if(!exp) return '<span class="badge bg-secondary">never</span>';
|
|
if(Date.now() > exp) return '<span class="badge bg-danger">expired</span>';
|
|
return '<span class="badge bg-warning text-dark">' + moment(exp, "x").fromNow() + '</span>';
|
|
}
|
|
|
|
function processToken(token){
|
|
tokensById[token.id] = token;
|
|
token.id_short = token.id.slice(0, 12) + '…';
|
|
token.expires_display = fmtExpiry(token);
|
|
token.created_display = fmtTime(token.created_on);
|
|
token.last_used_display = fmtTime(token.last_used_on);
|
|
return token;
|
|
}
|
|
|
|
async function tableAJAX(){
|
|
let data = await app.apiToken.list();
|
|
$.scope.apiTokenCard.empty();
|
|
$.each(data.results, function(_, token){
|
|
$.scope.apiTokenCard.push(processToken(token));
|
|
});
|
|
}
|
|
|
|
async function revokeToken(id, name, btn){
|
|
var $card = $(btn).closest('.card');
|
|
$card.addClass('table-warning');
|
|
var confirmed = await app.util.actionConfirm('Revoke API token "' + name + '"? It stops working immediately.', $card, 'warning');
|
|
$card.removeClass('table-warning');
|
|
if(!confirmed) return;
|
|
app.apiToken.remove({id: id}, function(error, data){
|
|
if(error){ app.util.actionMessage('Error: ' + data.message, $card, 'danger'); return; }
|
|
$.scope.apiTokenCard.remove('id', id);
|
|
});
|
|
}
|
|
|
|
async function rotateToken(id, name, btn){
|
|
var $card = $(btn).closest('.card');
|
|
var confirmed = await app.util.actionConfirm('Rotate API token "' + name + '"? The old token stops working immediately.', $card, 'warning');
|
|
if(!confirmed) return;
|
|
app.apiToken.rotate({id: id}, function(error, data){
|
|
if(error){ app.util.actionMessage('Error: ' + data.message, $card, 'danger'); return; }
|
|
showSecret(data.token);
|
|
tableAJAX();
|
|
});
|
|
}
|
|
|
|
function editToken(id){
|
|
var t = tokensById[id]; if(!t) return;
|
|
$('#edit-id').val(id);
|
|
$('#edit-name').val(t.name || '');
|
|
$('#edit-description').val(t.description || '');
|
|
$('#edit-expires_in_days').val('');
|
|
editModal.show();
|
|
}
|
|
|
|
function saveEdit(btn){
|
|
var $msg = $('#editModal .actionMessage');
|
|
var payload = {
|
|
id: $('#edit-id').val(),
|
|
name: $('#edit-name').val(),
|
|
description: $('#edit-description').val(),
|
|
expires_in_days: $('#edit-expires_in_days').val(),
|
|
};
|
|
app.apiToken.update(payload, function(error, data){
|
|
if(error){ app.util.actionMessage((data && data.message) || 'Update failed.', $msg.parent(), 'danger'); return; }
|
|
editModal.hide();
|
|
tableAJAX();
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<!-- Not class="row": app-base.js's `$('div.row').fadeIn('slow')` page-reveal
|
|
runs before this page's own ready handler and would unhide any div.row
|
|
unconditionally, defeating the isOwnProfile check below. -->
|
|
<div id="own-api-tokens-section" style="display:none">
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<h5 class="mb-3"><i class="fa-solid fa-code"></i> API Tokens</h5>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card shadow-lg">
|
|
<div class="card-header"><i class="fa-solid fa-plus"></i> New API Token</div>
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
<div class="card-body">
|
|
<p class="text-muted small">A personal access token lets scripts and services call the SSO management API as you, with your permissions. Treat it like a password.</p>
|
|
<form action="api-token/" method="post" onsubmit="formAJAX(this)">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control shadow" name="name" placeholder="CI user sync" validate=":1">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<input type="text" class="form-control shadow" name="description" placeholder="Used by the nightly sync job">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Expires in (days) <small class="text-muted">(0 = never)</small></label>
|
|
<input type="number" class="form-control shadow" name="expires_in_days" value="0" min="0">
|
|
</div>
|
|
<button type="submit" class="btn btn-outline-dark"><i class="fa-solid fa-plus"></i> Create</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
|
|
<div jq-repeat="apiTokenCard" jq-index-key="id" id="apitoken-card-{{id}}" class="card shadow mb-3">
|
|
<div class="card-header">
|
|
<h5><i class="fa-solid fa-key"></i> {{ name }}</h5>
|
|
<small class="text-muted font-monospace">{{ id_short }}</small>
|
|
</div>
|
|
<div class="card-header actionMessage" style="display:none"></div>
|
|
<div class="card-body">
|
|
{{ #description }}<p>{{ description }}</p>{{ /description }}
|
|
<dl class="row mb-0">
|
|
<dt class="col-sm-3">Token ID</dt>
|
|
<dd class="col-sm-9"><code>{{ id_short }}</code></dd>
|
|
<dt class="col-sm-3">Created</dt>
|
|
<dd class="col-sm-9">{{{ created_display }}}</dd>
|
|
<dt class="col-sm-3">Last used</dt>
|
|
<dd class="col-sm-9">{{{ last_used_display }}}</dd>
|
|
<dt class="col-sm-3">Expires</dt>
|
|
<dd class="col-sm-9">{{{ expires_display }}}</dd>
|
|
</dl>
|
|
</div>
|
|
<div class="card-footer">
|
|
<button type="button" onclick="editToken('{{id}}')" class="btn btn-primary btn-sm"><i class="fa-solid fa-pen-to-square"></i> Edit</button>
|
|
<button type="button" onclick="rotateToken('{{id}}', '{{name}}', this)" class="btn btn-warning btn-sm"><i class="fa-solid fa-arrows-rotate"></i> Rotate</button>
|
|
<button type="button" onclick="revokeToken('{{id}}', '{{name}}', this)" class="btn btn-danger btn-sm float-end"><i class="fa-solid fa-trash"></i> Revoke</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<%- include('impersonate_modal') %>
|
|
<%- include('bottom') %>
|