mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: token editing and deletion
This commit is contained in:
@@ -16,7 +16,11 @@
|
|||||||
"last-seen": "Last seen",
|
"last-seen": "Last seen",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"create-token": "Create Token",
|
"create-token": "Create Token",
|
||||||
|
"update-token": "Update Token",
|
||||||
"master-token": "Master token",
|
"master-token": "Master token",
|
||||||
"last-seen-never": "This key has never been used.",
|
"last-seen-never": "This key has never been used.",
|
||||||
"no-description": "No description specified."
|
"no-description": "No description specified.",
|
||||||
|
"actions": "Actions",
|
||||||
|
|
||||||
|
"delete-confirm": "Are you sure you wish to delete this token? It will not be recoverable."
|
||||||
}
|
}
|
||||||
@@ -13,66 +13,155 @@ define('admin/settings/api', ['settings', 'clipboard', 'bootbox', 'benchpress',
|
|||||||
const copyEls = document.querySelectorAll('[data-component="acp/tokens"] [data-action="copy"]');
|
const copyEls = document.querySelectorAll('[data-component="acp/tokens"] [data-action="copy"]');
|
||||||
new clipboard(copyEls);
|
new clipboard(copyEls);
|
||||||
|
|
||||||
handleTokenCreation();
|
handleActions();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleTokenCreation() {
|
function handleActions() {
|
||||||
const createEl = document.querySelector('[data-action="create"]');
|
const formEl = document.querySelector('#content form');
|
||||||
if (createEl) {
|
if (!formEl) {
|
||||||
createEl.addEventListener('click', async () => {
|
return;
|
||||||
const html = await Benchpress.render('admin/partials/edit-token-modal', {});
|
|
||||||
bootbox.dialog({
|
|
||||||
title: '[[admin/settings/api:create-token]]',
|
|
||||||
message: html,
|
|
||||||
buttons: {
|
|
||||||
submit: {
|
|
||||||
label: '[[modules:bootbox.submit]]',
|
|
||||||
className: 'btn-primary',
|
|
||||||
callback: parseCreateForm,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formEl.addEventListener('click', (e) => {
|
||||||
|
const subselector = e.target.closest('[data-action]');
|
||||||
|
if (subselector) {
|
||||||
|
const action = subselector.getAttribute('data-action');
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'create':
|
||||||
|
handleTokenCreation();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
handleTokenUpdate(subselector);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
handleTokenDeletion(subselector);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseCreateForm() {
|
async function handleTokenCreation() {
|
||||||
const modal = this;
|
const html = await Benchpress.render('admin/partials/edit-token-modal', {});
|
||||||
const formEl = this.get(0).querySelector('form');
|
const parseForm = async function () {
|
||||||
const tokensTableBody = document.querySelector('[data-component="acp/tokens"] tbody');
|
const modal = this;
|
||||||
const valid = formEl.reportValidity();
|
const formEl = this.get(0).querySelector('form');
|
||||||
if (formEl && valid) {
|
const tokensTableBody = document.querySelector('[data-component="acp/tokens"] tbody');
|
||||||
const formData = new FormData(formEl);
|
const valid = formEl.reportValidity();
|
||||||
const uid = formData.get('uid');
|
if (formEl && valid) {
|
||||||
const description = formData.get('description');
|
const formData = new FormData(formEl);
|
||||||
// const qs = new URLSearchParams(payload).toString();
|
const uid = formData.get('uid');
|
||||||
|
const description = formData.get('description');
|
||||||
|
|
||||||
const token = await api.post('/admin/tokens', { uid, description }).catch(app.alertError);
|
try {
|
||||||
|
const token = await api.post('/admin/tokens', { uid, description });
|
||||||
|
|
||||||
const now = new Date();
|
if (!tokensTableBody) {
|
||||||
const rowEl = (await app.parseAndTranslate(ajaxify.data.template.name, 'tokens', {
|
modal.modal('hide');
|
||||||
tokens: [{
|
return ajaxify.refresh();
|
||||||
token,
|
}
|
||||||
uid,
|
|
||||||
description,
|
|
||||||
timestamp: now.getTime(),
|
|
||||||
timestampISO: now.toISOString(),
|
|
||||||
lastSeen: null,
|
|
||||||
lastSeenISO: new Date(0).toISOString(),
|
|
||||||
}],
|
|
||||||
})).get(0);
|
|
||||||
|
|
||||||
if (tokensTableBody) {
|
const now = new Date();
|
||||||
tokensTableBody.append(rowEl);
|
const tokenObj = {
|
||||||
$(rowEl).find('.timeago').timeago();
|
token,
|
||||||
} else {
|
uid,
|
||||||
ajaxify.refresh();
|
description,
|
||||||
|
timestamp: now.getTime(),
|
||||||
|
timestampISO: now.toISOString(),
|
||||||
|
lastSeen: null,
|
||||||
|
lastSeenISO: new Date(0).toISOString(),
|
||||||
|
};
|
||||||
|
ajaxify.data.tokens.append(tokenObj);
|
||||||
|
const rowEl = (await app.parseAndTranslate(ajaxify.data.template.name, 'tokens', {
|
||||||
|
tokens: [tokenObj],
|
||||||
|
})).get(0);
|
||||||
|
|
||||||
|
tokensTableBody.append(rowEl);
|
||||||
|
$(rowEl).find('.timeago').timeago();
|
||||||
|
modal.modal('hide');
|
||||||
|
} catch (e) {
|
||||||
|
app.alertError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.modal('hide');
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
return false;
|
bootbox.dialog({
|
||||||
|
title: '[[admin/settings/api:create-token]]',
|
||||||
|
message: html,
|
||||||
|
buttons: {
|
||||||
|
submit: {
|
||||||
|
label: '[[modules:bootbox.submit]]',
|
||||||
|
className: 'btn-primary',
|
||||||
|
callback: parseForm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTokenUpdate(el) {
|
||||||
|
const rowEl = el.closest('[data-token]');
|
||||||
|
const token = rowEl.getAttribute('data-token');
|
||||||
|
const { uid, description } = await api.get(`/admin/tokens/${token}`);
|
||||||
|
const parseForm = async function () {
|
||||||
|
const modal = this;
|
||||||
|
const formEl = this.get(0).querySelector('form');
|
||||||
|
const valid = formEl.reportValidity();
|
||||||
|
if (formEl && valid) {
|
||||||
|
const formData = new FormData(formEl);
|
||||||
|
const uid = formData.get('uid');
|
||||||
|
const description = formData.get('description');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenObj = await api.put(`/admin/tokens/${token}`, { uid, description });
|
||||||
|
const newEl = (await app.parseAndTranslate(ajaxify.data.template.name, 'tokens', {
|
||||||
|
tokens: [tokenObj],
|
||||||
|
})).get(0);
|
||||||
|
|
||||||
|
rowEl.replaceWith(newEl);
|
||||||
|
$(newEl).find('.timeago').timeago();
|
||||||
|
modal.modal('hide');
|
||||||
|
} catch (e) {
|
||||||
|
app.alertError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = await Benchpress.render('admin/partials/edit-token-modal', { uid, description });
|
||||||
|
bootbox.dialog({
|
||||||
|
title: '[[admin/settings/api:update-token]]',
|
||||||
|
message: html,
|
||||||
|
buttons: {
|
||||||
|
submit: {
|
||||||
|
label: '[[modules:bootbox.submit]]',
|
||||||
|
className: 'btn-primary',
|
||||||
|
callback: parseForm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTokenDeletion(el) {
|
||||||
|
const rowEl = el.closest('[data-token]');
|
||||||
|
const token = rowEl.getAttribute('data-token');
|
||||||
|
|
||||||
|
bootbox.confirm('[[admin/settings/api:delete-confirm]]', async (ok) => {
|
||||||
|
if (ok) {
|
||||||
|
try {
|
||||||
|
await api.del(`/admin/tokens/${token}`);
|
||||||
|
} catch (e) {
|
||||||
|
app.alertError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowEl.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ACP;
|
return ACP;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ Admin.generateToken = async (req, res) => {
|
|||||||
helpers.formatApiResponse(200, res, await api.utils.tokens.generate({ uid, description }));
|
helpers.formatApiResponse(200, res, await api.utils.tokens.generate({ uid, description }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Admin.getToken = async (req, res) => {
|
||||||
|
helpers.formatApiResponse(200, res, await api.utils.tokens.get(req.params.token));
|
||||||
|
};
|
||||||
|
|
||||||
Admin.updateToken = async (req, res) => {
|
Admin.updateToken = async (req, res) => {
|
||||||
// todo: token rolling via req.body
|
// todo: token rolling via req.body
|
||||||
const { uid, description } = req.body;
|
const { uid, description } = req.body;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ module.exports = function () {
|
|||||||
setupApiRoute(router, 'get', '/analytics/:set', [...middlewares], controllers.write.admin.getAnalyticsData);
|
setupApiRoute(router, 'get', '/analytics/:set', [...middlewares], controllers.write.admin.getAnalyticsData);
|
||||||
|
|
||||||
setupApiRoute(router, 'post', '/tokens', [...middlewares], controllers.write.admin.generateToken);
|
setupApiRoute(router, 'post', '/tokens', [...middlewares], controllers.write.admin.generateToken);
|
||||||
|
setupApiRoute(router, 'get', '/tokens/:token', [...middlewares], controllers.write.admin.getToken);
|
||||||
setupApiRoute(router, 'put', '/tokens/:token', [...middlewares], controllers.write.admin.updateToken);
|
setupApiRoute(router, 'put', '/tokens/:token', [...middlewares], controllers.write.admin.updateToken);
|
||||||
setupApiRoute(router, 'delete', '/tokens/:token', [...middlewares], controllers.write.admin.deleteToken);
|
setupApiRoute(router, 'delete', '/tokens/:token', [...middlewares], controllers.write.admin.deleteToken);
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,12 @@
|
|||||||
<th>[[admin/settings/api:uid]]</th>
|
<th>[[admin/settings/api:uid]]</th>
|
||||||
<th>[[admin/settings/api:last-seen]]</th>
|
<th>[[admin/settings/api:last-seen]]</th>
|
||||||
<th>[[admin/settings/api:created]]</th>
|
<th>[[admin/settings/api:created]]</th>
|
||||||
|
<th>[[admin/settings/api:actions]]</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{{ each tokens }}}
|
{{{ each tokens }}}
|
||||||
<tr>
|
<tr data-token="{./token}">
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-link" data-action="copy" data-clipboard-text="{./token}"><i class="fa fa-fw fa-clipboard" aria-hidden="true"></i></button>
|
<button type="button" class="btn btn-link" data-action="copy" data-clipboard-text="{./token}"><i class="fa fa-fw fa-clipboard" aria-hidden="true"></i></button>
|
||||||
<div class="vr me-3" aria-hidden="true"></div>
|
<div class="vr me-3" aria-hidden="true"></div>
|
||||||
@@ -55,6 +56,14 @@
|
|||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<span class="timeago" title="{./timestampISO}"></span>
|
<span class="timeago" title="{./timestampISO}"></span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-link" data-action="edit">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-link link-danger" data-action="delete">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user