diff --git a/dDashboardServer/static/js/api-keys.js b/dDashboardServer/static/js/api-keys.js new file mode 100644 index 000000000..2fa685296 --- /dev/null +++ b/dDashboardServer/static/js/api-keys.js @@ -0,0 +1,163 @@ +// API keys on the account page: list, make (with a permission picker of the maker's own permissions), rotate, revoke +(function () { + var card = document.getElementById('apiKeys'); + if (!card) return; + var accountId = card.dataset.account; + var isSelf = card.dataset.self === '1'; + var canCreate = card.dataset.canCreate === '1'; + var $ = function (id) { return document.getElementById(id); }; + var catalog = null; + var titles = {}; + + var STATUS = { + active: ['Active', 'success'], + revoked: ['Revoked', 'secondary'], + expired: ['Expired', 'secondary'], + signed_out: ['Stopped by sign out', 'warning'] + }; + + function loadCatalog() { + if (catalog) return Promise.resolve(catalog); + return api.get('/api/api_keys/permissions').then(function (d) { + if (!d.success) throw new Error(d.error || 'Could not load permissions'); + catalog = d; + d.permissions.forEach(function (p) { titles[p.key] = p.title; }); + return d; + }); + } + + function scopeCell(k) { + var html = k.allPermissions ? fmt.badge('All of ' + (isSelf ? 'your' : 'their') + ' permissions', 'primary') + : '' + esc(k.permissions.length) + ' permission' + (k.permissions.length === 1 ? '' : 's') + ''; + if (k.readOnly) html += ' ' + fmt.badge('read-only', 'info'); + if (k.lostPermissions.length) { + html += '
' + esc(k.prefix) + '…' +
+ (k.note ? 'You have no permissions to give a key.
'; + $('apiKeyRate').placeholder = 'Default (' + catalog.defaultRateLimit + ')'; + $('apiKeyRate').max = catalog.maxRateLimit; + } + + function showForm(show) { + $('apiKeyForm').classList.toggle('d-none', !show); + if (show) loadCatalog().then(renderPicker).catch(function (e) { toast(e.message, 'danger'); }); + } + + function showSecret(key) { + $('apiKeySecret').value = key; + $('apiKeyCreated').classList.remove('d-none'); + $('apiKeySecret').select(); + } + + if ($('apiKeyNew')) $('apiKeyNew').addEventListener('click', function () { showForm(true); }); + $('apiKeyCancel').addEventListener('click', function () { showForm(false); }); + $('apiKeyCopy').addEventListener('click', function () { + var box = $('apiKeySecret'); + box.select(); + if (navigator.clipboard) navigator.clipboard.writeText(box.value).then(function () { toast('Copied', 'success'); }); + }); + $('apiKeyScopeAll').addEventListener('change', function () { $('apiKeyPicker').classList.add('d-none'); }); + $('apiKeyScopeSome').addEventListener('change', function () { $('apiKeyPicker').classList.remove('d-none'); }); + $('apiKeyPicker').addEventListener('click', function (e) { + var group = e.target.dataset.group; + if (group === undefined) return; + var boxes = $('apiKeyPicker').querySelectorAll('[data-group-of="' + group + '"]'); + var check = Array.prototype.some.call(boxes, function (b) { return !b.checked; }); + boxes.forEach(function (b) { b.checked = check; }); + }); + + $('apiKeyForm').addEventListener('submit', function (e) { + e.preventDefault(); + var all = $('apiKeyScopeAll').checked; + var picked = Array.prototype.map.call($('apiKeyPicker').querySelectorAll('input:checked'), function (b) { return b.value; }); + if (!all && !picked.length) { toast('Pick at least one permission, or all of yours', 'warning'); return; } + api.action('/api/api_keys', { + name: $('apiKeyName').value, + note: $('apiKeyNote').value, + permissions: all ? '*' : picked, + readOnly: $('apiKeyReadOnly').checked, + allowedIps: $('apiKeyIps').value, + allowedPaths: $('apiKeyPaths').value, + rateLimit: parseInt($('apiKeyRate').value, 10) || 0, + dailyQuota: parseInt($('apiKeyQuota').value, 10) || 0, + expiresInDays: parseInt($('apiKeyExpiry').value, 10) || 0 + }).then(function (d) { + $('apiKeyForm').reset(); + $('apiKeyPicker').classList.remove('d-none'); + showForm(false); + showSecret(d.key); + load(); + }).catch(function () {}); + }); + + $('apiKeyRows').addEventListener('click', function (e) { + var rotate = e.target.dataset.rotate, revoke = e.target.dataset.revoke; + if (rotate) { + if (!confirm('Make a new secret for this key? The current one stops working at once.')) return; + api.action('/api/api_keys/' + rotate + '/rotate', {}).then(function (d) { showSecret(d.key); load(); }).catch(function () {}); + } else if (revoke) { + if (!confirm('Revoke this key? Anything using it stops working at once.')) return; + api.action('/api/api_keys/' + revoke + '/revoke', {}, 'API key revoked').then(load).catch(function () {}); + } + }); + + load(); +})(); diff --git a/dDashboardServer/templates/account-view.jinja2 b/dDashboardServer/templates/account-view.jinja2 index 52caa9f69..264591ee7 100644 --- a/dDashboardServer/templates/account-view.jinja2 +++ b/dDashboardServer/templates/account-view.jinja2 @@ -297,26 +297,9 @@A bearer token for scripts and bots. It acts as you, with your current GM level. See API docs.
-API access isn't turned on for your account.
- {% endif %} -Lost a token, or signed in on a computer that isn't yours? This signs out every other browser and stops every API token you made.
+Signed in on a computer that isn't yours, or lost an API key? This signs out every other browser and stops every API key you made (rotate a key to start it again).
Keys for scripts and bots (Authorization: Bearer <key>, see the API docs). A key does only what you pick for it, and never more than you can do right now: if your GM level or permissions change, your keys change with them.{% if not can.api_access %} API access isn't turned on for your account, so you can't make keys.{% endif %}
| Name | Can do | Limits | Last used | Requests | Expires | Status | |
|---|---|---|---|---|---|---|---|
| Loading... | |||||||