|
726 | 726 | </div> |
727 | 727 | </div> |
728 | 728 | </div> |
| 729 | + |
| 730 | + <!-- API Tokens (only when password is set) --> |
| 731 | + {% if config.admin_password %} |
| 732 | + <div class="settings-card" id="api-tokens-card"> |
| 733 | + <div class="settings-card-header"> |
| 734 | + <div class="settings-card-icon"><i data-lucide="key-round"></i></div> |
| 735 | + <div> |
| 736 | + <div class="settings-card-title">{{ t.api_tokens_title|default('API Tokens') }}</div> |
| 737 | + <div class="settings-card-desc">{{ t.api_tokens_desc|default('Create tokens for programmatic API access') }}</div> |
| 738 | + </div> |
| 739 | + </div> |
| 740 | + <div style="padding: 0 16px 16px;"> |
| 741 | + <div style="display: flex; gap: 8px; margin-bottom: 12px;"> |
| 742 | + <input type="text" id="api-token-name" placeholder="{{ t.api_token_name_placeholder|default('e.g. Home Assistant') }}" style="flex: 1;"> |
| 743 | + <button type="button" class="btn btn-primary" onclick="createApiToken()"> |
| 744 | + <i data-lucide="plus" style="width:16px;height:16px;"></i> |
| 745 | + {{ t.api_token_create|default('Create Token') }} |
| 746 | + </button> |
| 747 | + </div> |
| 748 | + <div id="api-token-created-banner" style="display: none; background: var(--card-bg, #1a1d23); border: 1px solid var(--accent, #4f8cff); border-radius: 8px; padding: 12px; margin-bottom: 12px;"> |
| 749 | + <div style="font-weight: 600; margin-bottom: 4px;">{{ t.api_token_created|default('Token created. Copy it now.') }}</div> |
| 750 | + <div style="display: flex; gap: 8px; align-items: center;"> |
| 751 | + <code id="api-token-plaintext" style="flex: 1; word-break: break-all; font-size: 0.85em; padding: 6px 8px; background: var(--bg, #12141a); border-radius: 4px;"></code> |
| 752 | + <button type="button" class="btn" onclick="copyToken()" style="white-space: nowrap;"> |
| 753 | + <i data-lucide="copy" style="width:14px;height:14px;"></i> |
| 754 | + </button> |
| 755 | + </div> |
| 756 | + <div style="font-size: 0.8em; opacity: 0.7; margin-top: 6px;">{{ t.api_token_copy_warning|default('This token will only be shown once.') }}</div> |
| 757 | + </div> |
| 758 | + <table id="api-tokens-table" style="width: 100%; font-size: 0.9em; display: none;"> |
| 759 | + <thead> |
| 760 | + <tr style="text-align: left; opacity: 0.7;"> |
| 761 | + <th style="padding: 4px 8px;">{{ t.api_token_name|default('Name') }}</th> |
| 762 | + <th style="padding: 4px 8px;">{{ t.api_token_prefix|default('Token') }}</th> |
| 763 | + <th style="padding: 4px 8px;">{{ t.api_token_last_used|default('Last Used') }}</th> |
| 764 | + <th style="padding: 4px 8px;"></th> |
| 765 | + </tr> |
| 766 | + </thead> |
| 767 | + <tbody id="api-tokens-body"></tbody> |
| 768 | + </table> |
| 769 | + <div id="api-tokens-empty" style="opacity: 0.6; font-size: 0.9em;">{{ t.api_tokens_none|default('No API tokens created yet.') }}</div> |
| 770 | + </div> |
| 771 | + </div> |
| 772 | + {% endif %} |
729 | 773 | </div> |
730 | 774 |
|
731 | 775 | <!-- ═══ Panel: Backup ═══ --> |
|
1173 | 1217 | saveFooter.style.display = (id === 'support') ? 'none' : ''; |
1174 | 1218 | } |
1175 | 1219 |
|
| 1220 | + if (id === 'general') loadApiTokens(); |
1176 | 1221 | history.replaceState(null, '', '#' + id); |
1177 | 1222 | closeMobileSidebar(); |
1178 | 1223 | } |
1179 | 1224 |
|
| 1225 | +/* ── API Token Management ── */ |
| 1226 | +function _tokenCell(text, style) { |
| 1227 | + var td = document.createElement('td'); |
| 1228 | + td.style.cssText = style || 'padding:4px 8px;'; |
| 1229 | + td.textContent = text; |
| 1230 | + return td; |
| 1231 | +} |
| 1232 | + |
| 1233 | +function loadApiTokens() { |
| 1234 | + var table = document.getElementById('api-tokens-table'); |
| 1235 | + var body = document.getElementById('api-tokens-body'); |
| 1236 | + var empty = document.getElementById('api-tokens-empty'); |
| 1237 | + if (!table || !body) return; |
| 1238 | + fetch('/api/tokens').then(function(r) { return r.json(); }).then(function(data) { |
| 1239 | + var tokens = (data.tokens || []).filter(function(t) { return !t.revoked; }); |
| 1240 | + while (body.firstChild) body.removeChild(body.firstChild); |
| 1241 | + if (tokens.length === 0) { |
| 1242 | + table.style.display = 'none'; |
| 1243 | + if (empty) empty.style.display = 'block'; |
| 1244 | + return; |
| 1245 | + } |
| 1246 | + table.style.display = 'table'; |
| 1247 | + if (empty) empty.style.display = 'none'; |
| 1248 | + tokens.forEach(function(tk) { |
| 1249 | + var tr = document.createElement('tr'); |
| 1250 | + tr.appendChild(_tokenCell(tk.name, 'padding:4px 8px;')); |
| 1251 | + var prefixTd = document.createElement('td'); |
| 1252 | + prefixTd.style.cssText = 'padding:4px 8px;'; |
| 1253 | + var code = document.createElement('code'); |
| 1254 | + code.textContent = tk.token_prefix + '...'; |
| 1255 | + prefixTd.appendChild(code); |
| 1256 | + tr.appendChild(prefixTd); |
| 1257 | + tr.appendChild(_tokenCell(tk.last_used_at || '\u2014', 'padding:4px 8px;')); |
| 1258 | + var actionTd = document.createElement('td'); |
| 1259 | + actionTd.style.cssText = 'padding:4px 8px;'; |
| 1260 | + var btn = document.createElement('button'); |
| 1261 | + btn.type = 'button'; |
| 1262 | + btn.className = 'btn btn-sm'; |
| 1263 | + btn.style.cssText = 'font-size:0.8em;padding:2px 8px;'; |
| 1264 | + btn.textContent = T.api_token_revoke || 'Revoke'; |
| 1265 | + btn.setAttribute('data-token-id', tk.id); |
| 1266 | + btn.setAttribute('data-token-name', tk.name); |
| 1267 | + btn.addEventListener('click', function() { |
| 1268 | + revokeToken(parseInt(this.getAttribute('data-token-id')), this.getAttribute('data-token-name')); |
| 1269 | + }); |
| 1270 | + actionTd.appendChild(btn); |
| 1271 | + tr.appendChild(actionTd); |
| 1272 | + body.appendChild(tr); |
| 1273 | + }); |
| 1274 | + }).catch(function() {}); |
| 1275 | +} |
| 1276 | + |
| 1277 | +function createApiToken() { |
| 1278 | + var inp = document.getElementById('api-token-name'); |
| 1279 | + var name = (inp.value || '').trim(); |
| 1280 | + if (!name) { inp.focus(); return; } |
| 1281 | + fetch('/api/tokens', { |
| 1282 | + method: 'POST', |
| 1283 | + headers: {'Content-Type': 'application/json'}, |
| 1284 | + body: JSON.stringify({name: name}) |
| 1285 | + }).then(function(r) { return r.json().then(function(d) { return {ok: r.ok, data: d}; }); }) |
| 1286 | + .then(function(res) { |
| 1287 | + if (!res.ok) { showToast(res.data.error || 'Error', false); return; } |
| 1288 | + inp.value = ''; |
| 1289 | + var banner = document.getElementById('api-token-created-banner'); |
| 1290 | + document.getElementById('api-token-plaintext').textContent = res.data.token; |
| 1291 | + banner.style.display = 'block'; |
| 1292 | + loadApiTokens(); |
| 1293 | + if (typeof lucide !== 'undefined') lucide.createIcons(); |
| 1294 | + }).catch(function() { showToast('Error', false); }); |
| 1295 | +} |
| 1296 | + |
| 1297 | +function copyToken() { |
| 1298 | + var text = document.getElementById('api-token-plaintext').textContent; |
| 1299 | + navigator.clipboard.writeText(text).then(function() { |
| 1300 | + showToast(T.api_token_copied || 'Token copied!', true); |
| 1301 | + }); |
| 1302 | +} |
| 1303 | + |
| 1304 | +function revokeToken(id, name) { |
| 1305 | + var msg = (T.api_token_revoke_confirm || 'Revoke token "{name}"?').replace('{name}', name); |
| 1306 | + if (!confirm(msg)) return; |
| 1307 | + fetch('/api/tokens/' + id, {method: 'DELETE'}) |
| 1308 | + .then(function(r) { return r.json(); }) |
| 1309 | + .then(function(data) { |
| 1310 | + if (data.success) { |
| 1311 | + showToast(T.api_token_revoked || 'Token revoked', true); |
| 1312 | + document.getElementById('api-token-created-banner').style.display = 'none'; |
| 1313 | + loadApiTokens(); |
| 1314 | + } else { |
| 1315 | + showToast(data.error || 'Error', false); |
| 1316 | + } |
| 1317 | + }).catch(function() { showToast('Error', false); }); |
| 1318 | +} |
| 1319 | + |
1180 | 1320 | /* Restore section from URL hash */ |
1181 | 1321 | (function() { |
1182 | 1322 | var hash = location.hash.replace('#', ''); |
|
0 commit comments