Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 599136e4dc | |||
| a7d5efc764 | |||
| 8a76f71edd | |||
| e482f52f10 |
@@ -4,6 +4,19 @@ All notable changes to this project are documented here. Format loosely
|
|||||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
|
||||||
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
||||||
|
|
||||||
|
## [1.10.0] - 2026-07-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **API-token UI unified with sso-manager-node/proxy**: card grid replacing the bare table, a new Edit modal (footer shows real created-by/on data), and a Description field on both the create and edit flows — the model and API already fully supported all of this, it just wasn't exposed anywhere in the dashboard.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `@simpleworkjs/frontend` bumped to `^0.2.6` (this app was still on `^0.2.5`).
|
||||||
|
|
||||||
|
## [1.9.0] - 2026-07-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **"Quick Jump" copy-to-clipboard section on the dashboard** — the `uid_-_target` grammar-mode SSH command was documented in the README but nowhere in the UI. A new card gives a one-click-copy command for interactive-picker mode, and every row in "Hosts you can reach" has its own copy button for the exact grammar-mode command to that host, ready to paste and run as-is (uses the logged-in user's own uid).
|
||||||
|
|
||||||
## [1.8.2] - 2026-07-28
|
## [1.8.2] - 2026-07-28
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+6
-6
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-jump-host",
|
"name": "t42-jump-host",
|
||||||
"version": "1.5.0",
|
"version": "1.9.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "t42-jump-host",
|
"name": "t42-jump-host",
|
||||||
"version": "1.5.0",
|
"version": "1.9.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
"@simpleworkjs/app-stack": "^1.0.0",
|
"@simpleworkjs/app-stack": "^1.0.0",
|
||||||
"@simpleworkjs/conf": "^1.2.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"@simpleworkjs/directory-schema": "^1.0.0",
|
"@simpleworkjs/directory-schema": "^1.0.0",
|
||||||
"@simpleworkjs/frontend": "^0.2.5",
|
"@simpleworkjs/frontend": "^0.2.6",
|
||||||
"@simpleworkjs/ldap": "^1.0.1",
|
"@simpleworkjs/ldap": "^1.0.1",
|
||||||
"@simpleworkjs/oidc-client": "^1.0.0",
|
"@simpleworkjs/oidc-client": "^1.0.0",
|
||||||
"@simpleworkjs/orm": "^0.2.8",
|
"@simpleworkjs/orm": "^0.2.8",
|
||||||
@@ -178,9 +178,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@simpleworkjs/frontend": {
|
"node_modules/@simpleworkjs/frontend": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@simpleworkjs/frontend/-/frontend-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@simpleworkjs/frontend/-/frontend-0.2.6.tgz",
|
||||||
"integrity": "sha512-PxR7UVPv3gRpdF0WsuAZplF1vYvKsEJQevVPhz9d72U+69vP/OH3tlaAXjtO/apMHfhT1viOPw2gMVOrPSxYZw==",
|
"integrity": "sha512-2uqvEjxyZ2LE+sfhP6rJcEMmqdViazJ3ZkitWJXInPMWF6DiEZuP5MYqBqJvfDko63CCHEt1/ChFQd7Ry85Pzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-jump-host",
|
"name": "t42-jump-host",
|
||||||
"version": "1.8.2",
|
"version": "1.10.0",
|
||||||
"description": "SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics",
|
"description": "SSH jump host for the theta42 stack — LDAP-authenticated, directory-driven host bridging with audit and metrics",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"@simpleworkjs/app-stack": "^1.0.0",
|
"@simpleworkjs/app-stack": "^1.0.0",
|
||||||
"@simpleworkjs/conf": "^1.2.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"@simpleworkjs/directory-schema": "^1.0.0",
|
"@simpleworkjs/directory-schema": "^1.0.0",
|
||||||
"@simpleworkjs/frontend": "^0.2.5",
|
"@simpleworkjs/frontend": "^0.2.6",
|
||||||
"@simpleworkjs/ldap": "^1.0.1",
|
"@simpleworkjs/ldap": "^1.0.1",
|
||||||
"@simpleworkjs/oidc-client": "^1.0.0",
|
"@simpleworkjs/oidc-client": "^1.0.0",
|
||||||
"@simpleworkjs/orm": "^0.2.8",
|
"@simpleworkjs/orm": "^0.2.8",
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ app.jump = (function(app){
|
|||||||
app.apiToken = (function(app){
|
app.apiToken = (function(app){
|
||||||
function list(cb){ app.api.get('api-token/', cb); }
|
function list(cb){ app.api.get('api-token/', cb); }
|
||||||
function add(args, cb){ app.api.post('api-token/', args, cb); }
|
function add(args, cb){ app.api.post('api-token/', args, cb); }
|
||||||
|
function update(args, cb){ app.api.put('api-token/' + args.id, args, cb); }
|
||||||
function remove(id, cb){ app.api.delete('api-token/' + id, cb); }
|
function remove(id, cb){ app.api.delete('api-token/' + id, cb); }
|
||||||
function rotate(id, cb){ app.api.post('api-token/' + id + '/rotate', {}, cb); }
|
function rotate(id, cb){ app.api.post('api-token/' + id + '/rotate', {}, cb); }
|
||||||
return {list: list, add: add, remove: remove, rotate: rotate};
|
return {list: list, add: add, update: update, remove: remove, rotate: rotate};
|
||||||
})(app);
|
})(app);
|
||||||
|
|
||||||
// Shared render helpers.
|
// Shared render helpers.
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const values = {
|
|||||||
titleIcon: conf.environment !== 'production' ? '<i class="fa-brands fa-dev"></i>' : '',
|
titleIcon: conf.environment !== 'production' ? '<i class="fa-brands fa-dev"></i>' : '',
|
||||||
name: conf.name,
|
name: conf.name,
|
||||||
logo: conf.logo,
|
logo: conf.logo,
|
||||||
|
// The SSH front door's port -- the dashboard's "quick jump" copy buttons
|
||||||
|
// need this to build a real, working `ssh ...` command (the web UI and
|
||||||
|
// SSH front door share a hostname but not a port).
|
||||||
|
sshPort: (conf.ssh && conf.ssh.listenPort) || 22,
|
||||||
...buildInfo,
|
...buildInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+156
-32
@@ -28,6 +28,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header"><i class="fa-solid fa-terminal me-1"></i> Quick Jump</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small mb-2">
|
||||||
|
Skip the picker: <code>ssh <your-username>_-_<host-slug>@<this-jump-host></code>
|
||||||
|
connects straight to a host. Or just <code>ssh <your-username>@<this-jump-host></code>
|
||||||
|
for the interactive picker.
|
||||||
|
</p>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control font-monospace" id="quick-jump-cmd" readonly>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="copyFieldValue('#quick-jump-cmd')" title="Copy">
|
||||||
|
<i class="fa-solid fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
@@ -63,10 +84,36 @@
|
|||||||
(e.g. <code>GET /api/user/hosts</code>) — not for SSH login. A token carries
|
(e.g. <code>GET /api/user/hosts</code>) — not for SSH login. A token carries
|
||||||
no group claims, so it can't reach admin-only endpoints.
|
no group claims, so it can't reach admin-only endpoints.
|
||||||
</p>
|
</p>
|
||||||
<table class="table table-sm mb-0">
|
<div class="card-body">
|
||||||
<thead><tr><th>Name</th><th>Created</th><th>Last used</th><th>Expires</th><th></th></tr></thead>
|
<p id="api-tokens-empty" class="text-muted mb-0" style="display:none">No API tokens.</p>
|
||||||
<tbody id="api-tokens"></tbody>
|
<div id="api-tokens">
|
||||||
</table>
|
<div jq-repeat="apiTokenCard" jq-index-key="id" id="apitoken-card-{{id}}" class="card shadow-sm mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="fa-solid fa-key"></i> {{name}}</h6>
|
||||||
|
<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 small">
|
||||||
|
<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="rotateApiToken('{{id}}', this)" class="btn btn-warning btn-sm"><i class="fa-solid fa-arrows-rotate"></i> Rotate</button>
|
||||||
|
<button type="button" onclick="revokeApiToken('{{id}}', this)" class="btn btn-danger btn-sm float-end"><i class="fa-solid fa-trash"></i> Revoke</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,48 +126,79 @@
|
|||||||
$b.append('<tr><td>' + app.jump.esc(x.name) + '</td><td class="text-end">' + x.count + '</td></tr>');
|
$b.append('<tr><td>' + app.jump.esc(x.name) + '</td><td class="text-end">' + x.count + '</td></tr>');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// The web UI and the SSH front door share a hostname, just not a port.
|
||||||
|
var SSH_PORT = <%- JSON.stringify(sshPort) %>;
|
||||||
|
function sshCommand(target){
|
||||||
|
var uid = app.auth.user && app.auth.user.username;
|
||||||
|
if(!uid) return '';
|
||||||
|
var portFlag = SSH_PORT === 22 ? '' : ' -p ' + SSH_PORT;
|
||||||
|
return 'ssh ' + uid + (target ? '_-_' + target : '') + '@' + location.hostname + portFlag;
|
||||||
|
}
|
||||||
|
function copyFieldValue(sel){
|
||||||
|
var $el = $(sel);
|
||||||
|
var text = $el.val();
|
||||||
|
if(!text) return;
|
||||||
|
navigator.clipboard.writeText(text).then(function(){
|
||||||
|
app.messages.toast('Copied to clipboard', 'success');
|
||||||
|
}, function(){
|
||||||
|
app.messages.toast('Could not copy — select and copy manually', 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function hostRows(sel, hosts){
|
function hostRows(sel, hosts){
|
||||||
var $b = $(sel).empty();
|
var $b = $(sel).empty();
|
||||||
if(!hosts || !hosts.length){ $b.append('<tr><td class="text-muted">No hosts reachable.</td></tr>'); return; }
|
if(!hosts || !hosts.length){ $b.append('<tr><td class="text-muted">No hosts reachable.</td></tr>'); return; }
|
||||||
hosts.forEach(function(h){
|
hosts.forEach(function(h){
|
||||||
var addr = (h.metadata && (h.metadata.ip || h.metadata.address)) || '';
|
var addr = (h.metadata && (h.metadata.ip || h.metadata.address)) || '';
|
||||||
|
var rowId = 'host-cmd-' + h.slug.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||||
$b.append('<tr><td>' + app.jump.esc(h.displayName || h.name || h.slug) + '</td>'
|
$b.append('<tr><td>' + app.jump.esc(h.displayName || h.name || h.slug) + '</td>'
|
||||||
+ '<td class="text-muted small">' + app.jump.esc(h.slug) + '</td>'
|
+ '<td class="text-muted small">' + app.jump.esc(h.slug) + '</td>'
|
||||||
+ '<td class="text-end text-muted small">' + app.jump.esc(addr) + '</td></tr>');
|
+ '<td class="text-end text-muted small">' + app.jump.esc(addr) + '</td>'
|
||||||
|
+ '<td class="text-end">'
|
||||||
|
+ '<input type="hidden" id="' + rowId + '" value="' + app.jump.esc(sshCommand(h.slug)) + '">'
|
||||||
|
+ '<button class="btn btn-sm btn-outline-secondary" onclick="copyFieldValue(\'#' + rowId + '\')" title="Copy quick-jump command"><i class="fa-solid fa-copy"></i></button>'
|
||||||
|
+ '</td></tr>');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function tokenRows(tokens){
|
// expires_at/created_on/last_used_on come back as redis-hash strings for
|
||||||
var $b = $('#api-tokens').empty();
|
// some fields and real numbers for others depending on the model's field
|
||||||
if(!tokens || !tokens.length){ $b.append('<tr><td colspan="5" class="text-muted">No API tokens.</td></tr>'); return; }
|
// type -- fmtTime already handles both via moment(ms, 'x').
|
||||||
tokens.forEach(function(t){
|
function fmtExpiry(token){
|
||||||
var expires = t.expires_at ? app.jump.fmtTime(t.expires_at) : 'Never';
|
var exp = Number(token.expires_at);
|
||||||
var lastUsed = t.last_used_on ? app.jump.fmtTime(t.last_used_on) : 'Never';
|
if(!exp) return '<span class="badge text-bg-secondary">never</span>';
|
||||||
$b.append(
|
if(Date.now() > exp) return '<span class="badge text-bg-danger">expired</span>';
|
||||||
'<tr>'
|
return '<span class="badge text-bg-warning">' + moment(exp).fromNow() + '</span>';
|
||||||
+ '<td>' + app.jump.esc(t.name) + '</td>'
|
}
|
||||||
+ '<td class="text-muted small">' + app.jump.fmtTime(t.created_on) + '</td>'
|
|
||||||
+ '<td class="text-muted small">' + lastUsed + '</td>'
|
var tokensById = {};
|
||||||
+ '<td class="text-muted small">' + expires + '</td>'
|
function processToken(token){
|
||||||
+ '<td class="text-end">'
|
tokensById[token.id] = token;
|
||||||
+ '<button class="btn btn-sm btn-outline-secondary" onclick="rotateApiToken(\'' + t.id + '\', this)" title="Rotate"><i class="fa-solid fa-rotate"></i></button> '
|
token.id_short = token.id.slice(0, 12) + '…';
|
||||||
+ '<button class="btn btn-sm btn-outline-danger" onclick="revokeApiToken(\'' + t.id + '\', this)" title="Revoke"><i class="fa-solid fa-trash"></i></button>'
|
token.expires_display = fmtExpiry(token);
|
||||||
+ '</td>'
|
token.created_display = app.jump.fmtTime(token.created_on);
|
||||||
+ '</tr>'
|
token.last_used_display = token.last_used_on ? app.jump.fmtTime(token.last_used_on) : 'Never';
|
||||||
);
|
return token;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadApiTokens(){
|
function loadApiTokens(){
|
||||||
app.apiToken.list(function(error, data){
|
app.apiToken.list(function(error, data){
|
||||||
if(error) return tokenRows([]);
|
var tokens = (!error && data && data.results) || [];
|
||||||
tokenRows(data && data.results);
|
$.scope.apiTokenCard.empty();
|
||||||
|
tokens.forEach(function(t){ $.scope.apiTokenCard.push(processToken(t)); });
|
||||||
|
$('#api-tokens-empty').toggle(tokens.length === 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared "reveal secret once" display -- also used by proxy/sso-manager-node.
|
||||||
function showToken(title, token){
|
function showToken(title, token){
|
||||||
app.modal.open({title: title, bodyHtml:
|
app.modal.open({title: title, bodyHtml:
|
||||||
'<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>'
|
'<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" class="form-control font-monospace" readonly value="' + app.jump.esc(token) + '"></div>'
|
+ '<div class="input-group"><input type="text" class="form-control font-monospace" id="revealed-token" readonly value="' + app.jump.esc(token) + '">'
|
||||||
|
// Reuses the same copy-to-clipboard helper as the Quick Jump card
|
||||||
|
// above (toast feedback -- FontAwesome replaces <i> icons with
|
||||||
|
// inline <svg>, so a checkmark-flash-the-icon approach silently
|
||||||
|
// no-ops; the toast doesn't have that problem).
|
||||||
|
+ '<button class="btn btn-outline-secondary" onclick="copyFieldValue(\'#revealed-token\')" 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 ' + app.jump.esc(token) + '</code></p>'
|
+ '<p class="mt-3 mb-0 text-muted small">Use it as a bearer token:<br><code>Authorization: Bearer ' + app.jump.esc(token) + '</code></p>'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,27 +210,72 @@
|
|||||||
+ '<input type="text" class="form-control" id="new-token-name" placeholder="e.g. laptop-cron">'
|
+ '<input type="text" class="form-control" id="new-token-name" placeholder="e.g. laptop-cron">'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<div class="mb-3">'
|
+ '<div class="mb-3">'
|
||||||
|
+ '<label class="form-label">Description</label>'
|
||||||
|
+ '<input type="text" class="form-control" id="new-token-description" placeholder="optional">'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="mb-3">'
|
||||||
+ '<label class="form-label">Expires in (days, blank = never)</label>'
|
+ '<label class="form-label">Expires in (days, blank = never)</label>'
|
||||||
+ '<input type="number" class="form-control" id="new-token-days" min="1">'
|
+ '<input type="number" class="form-control" id="new-token-days" min="1">'
|
||||||
+ '</div>'
|
+ '</div>',
|
||||||
+ '<button class="btn btn-primary" onclick="submitApiToken()"><i class="fa-solid fa-check"></i> Create</button>'
|
footer: {buttonsHtml: app.modal.footerButtons({onSave: 'submitApiToken()', saveLabel: 'Create'})},
|
||||||
});
|
});
|
||||||
$body.find('#new-token-name').focus();
|
$body.find('#new-token-name').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitApiToken(){
|
function submitApiToken(){
|
||||||
var name = $('#new-token-name').val().trim();
|
var name = $('#new-token-name').val().trim();
|
||||||
var $card = $('#api-tokens').closest('.card');
|
if(!name) return app.messages.action('Name is required', app.modal.body(), 'danger');
|
||||||
if(!name) return app.messages.action('Name is required', $card, 'danger');
|
|
||||||
app.apiToken.add({
|
app.apiToken.add({
|
||||||
name: name,
|
name: name,
|
||||||
|
description: $('#new-token-description').val(),
|
||||||
expires_in_days: $('#new-token-days').val(),
|
expires_in_days: $('#new-token-days').val(),
|
||||||
}, function(error, data){
|
}, function(error, data){
|
||||||
if(error) return app.messages.action((data && data.message) || 'Failed to create token', $card, 'danger');
|
if(error) return app.messages.action((data && data.message) || 'Failed to create token', app.modal.body(), 'danger');
|
||||||
|
app.modal.close();
|
||||||
showToken('API Token Created', data.token);
|
showToken('API Token Created', data.token);
|
||||||
loadApiTokens();
|
loadApiTokens();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editToken(id){
|
||||||
|
var t = tokensById[id]; if(!t) return;
|
||||||
|
app.modal.open({
|
||||||
|
title: 'Edit Token',
|
||||||
|
bodyHtml:
|
||||||
|
'<input type="hidden" id="edit-token-id" value="' + app.jump.esc(id) + '">'
|
||||||
|
+ '<div class="mb-3">'
|
||||||
|
+ '<label class="form-label">Name</label>'
|
||||||
|
+ '<input type="text" class="form-control" id="edit-token-name" value="' + app.jump.esc(t.name || '') + '">'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="mb-3">'
|
||||||
|
+ '<label class="form-label">Description</label>'
|
||||||
|
+ '<input type="text" class="form-control" id="edit-token-description" value="' + app.jump.esc(t.description || '') + '">'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="mb-3">'
|
||||||
|
+ '<label class="form-label">Expires in (days, blank = keep as-is, 0 = never)</label>'
|
||||||
|
+ '<input type="number" class="form-control" id="edit-token-days" min="0">'
|
||||||
|
+ '</div>',
|
||||||
|
footer: {
|
||||||
|
metaHtml: 'Created by ' + app.jump.esc(t.created_by || '—') + ' on ' + app.jump.fmtTime(t.created_on),
|
||||||
|
buttonsHtml: app.modal.footerButtons({onSave: 'saveEditToken()', saveLabel: 'Save'}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEditToken(){
|
||||||
|
var payload = {
|
||||||
|
id: $('#edit-token-id').val(),
|
||||||
|
name: $('#edit-token-name').val(),
|
||||||
|
description: $('#edit-token-description').val(),
|
||||||
|
expires_in_days: $('#edit-token-days').val(),
|
||||||
|
};
|
||||||
|
app.apiToken.update(payload, function(error, data){
|
||||||
|
if(error) return app.messages.action((data && data.message) || 'Failed to update token', app.modal.body(), 'danger');
|
||||||
|
app.modal.close();
|
||||||
|
loadApiTokens();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function revokeApiToken(id, btn){
|
async function revokeApiToken(id, btn){
|
||||||
var $card = $(btn).closest('.card');
|
var $card = $(btn).closest('.card');
|
||||||
var ok = await app.messages.confirm('Revoke this API token? It stops working immediately.', $card, 'danger');
|
var ok = await app.messages.confirm('Revoke this API token? It stops working immediately.', $card, 'danger');
|
||||||
@@ -185,6 +308,7 @@
|
|||||||
});
|
});
|
||||||
await app.auth.loadUser();
|
await app.auth.loadUser();
|
||||||
if(app.auth.isAdmin()) $('#my-hosts-title').text('All hosts');
|
if(app.auth.isAdmin()) $('#my-hosts-title').text('All hosts');
|
||||||
|
$('#quick-jump-cmd').val(sshCommand());
|
||||||
app.jump.hosts(function(error, data){
|
app.jump.hosts(function(error, data){
|
||||||
if(error) return hostRows('#my-hosts', []);
|
if(error) return hostRows('#my-hosts', []);
|
||||||
hostRows('#my-hosts', data && data.results);
|
hostRows('#my-hosts', data && data.results);
|
||||||
|
|||||||
Reference in New Issue
Block a user