Spaces:
Paused
Paused
fix: clarify legacy api token recovery
Browse files
frontend/src/views/ApiTokensView.vue
CHANGED
|
@@ -99,11 +99,38 @@
|
|
| 99 |
</td>
|
| 100 |
<td>{{ formatDateTime(token.lastUsedAt) }}</td>
|
| 101 |
<td>{{ formatRestrictions(token.restrictions) }}</td>
|
| 102 |
-
<td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
<td>
|
| 104 |
<div class="form-actions">
|
| 105 |
-
<button
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
<button class="btn btn-ghost" type="button" @click="editToken(token)">编辑</button>
|
| 108 |
<button class="btn btn-ghost" type="button" @click="toggleToken(token)">
|
| 109 |
{{ token.enabled ? '禁用' : '启用' }}
|
|
@@ -317,7 +344,9 @@ async function rotateTokenForRow(token) {
|
|
| 317 |
error.value = '';
|
| 318 |
message.value = '';
|
| 319 |
|
| 320 |
-
const confirmed = window.confirm(
|
|
|
|
|
|
|
| 321 |
if (!confirmed) return;
|
| 322 |
|
| 323 |
try {
|
|
@@ -334,6 +363,9 @@ async function rotateTokenForRow(token) {
|
|
| 334 |
latestToken.value = nextToken;
|
| 335 |
latestTokenId.value = token.id;
|
| 336 |
latestTokenReason.value = 'Token 已重新生成';
|
|
|
|
|
|
|
|
|
|
| 337 |
message.value = 'API Token 已重新生成。旧 Token 已失效。';
|
| 338 |
await loadTokens();
|
| 339 |
} catch (err) {
|
|
|
|
| 99 |
</td>
|
| 100 |
<td>{{ formatDateTime(token.lastUsedAt) }}</td>
|
| 101 |
<td>{{ formatRestrictions(token.restrictions) }}</td>
|
| 102 |
+
<td>
|
| 103 |
+
<div class="file-col">
|
| 104 |
+
<strong>{{ token.tokenPreview }}</strong>
|
| 105 |
+
<small v-if="!token.recoverable">旧 Token,原文不可恢复</small>
|
| 106 |
+
</div>
|
| 107 |
+
</td>
|
| 108 |
<td>
|
| 109 |
<div class="form-actions">
|
| 110 |
+
<button
|
| 111 |
+
v-if="token.recoverable"
|
| 112 |
+
class="btn btn-ghost"
|
| 113 |
+
type="button"
|
| 114 |
+
@click="copyTokenForRow(token)"
|
| 115 |
+
>
|
| 116 |
+
复制 Token
|
| 117 |
+
</button>
|
| 118 |
+
<button
|
| 119 |
+
v-else
|
| 120 |
+
class="btn btn-ghost"
|
| 121 |
+
type="button"
|
| 122 |
+
@click="rotateTokenForRow(token)"
|
| 123 |
+
>
|
| 124 |
+
重新生成并复制
|
| 125 |
+
</button>
|
| 126 |
+
<button
|
| 127 |
+
v-if="token.recoverable"
|
| 128 |
+
class="btn btn-ghost"
|
| 129 |
+
type="button"
|
| 130 |
+
@click="rotateTokenForRow(token)"
|
| 131 |
+
>
|
| 132 |
+
重新生成
|
| 133 |
+
</button>
|
| 134 |
<button class="btn btn-ghost" type="button" @click="editToken(token)">编辑</button>
|
| 135 |
<button class="btn btn-ghost" type="button" @click="toggleToken(token)">
|
| 136 |
{{ token.enabled ? '禁用' : '启用' }}
|
|
|
|
| 344 |
error.value = '';
|
| 345 |
message.value = '';
|
| 346 |
|
| 347 |
+
const confirmed = window.confirm(token.recoverable
|
| 348 |
+
? '是否重新生成这个 Token?旧 Token 会立刻失效。'
|
| 349 |
+
: '这个旧 Token 无法恢复原文。是否立即重新生成并复制新的 Token?旧 Token 会立刻失效。');
|
| 350 |
if (!confirmed) return;
|
| 351 |
|
| 352 |
try {
|
|
|
|
| 363 |
latestToken.value = nextToken;
|
| 364 |
latestTokenId.value = token.id;
|
| 365 |
latestTokenReason.value = 'Token 已重新生成';
|
| 366 |
+
if (!token.recoverable) {
|
| 367 |
+
await copy(nextToken, '新 Token 已复制。');
|
| 368 |
+
}
|
| 369 |
message.value = 'API Token 已重新生成。旧 Token 已失效。';
|
| 370 |
await loadTokens();
|
| 371 |
} catch (err) {
|
server/lib/repos/api-token-repo.js
CHANGED
|
@@ -58,11 +58,13 @@ function hashSecret(secret, salt) {
|
|
| 58 |
|
| 59 |
function toPublicToken(row) {
|
| 60 |
if (!row) return null;
|
|
|
|
| 61 |
return {
|
| 62 |
id: row.id,
|
| 63 |
name: row.name,
|
| 64 |
scopes: JSON.parse(row.scopes_json || '[]'),
|
| 65 |
restrictions: JSON.parse(row.restrictions_json || '{}'),
|
|
|
|
| 66 |
expiresAt: row.expires_at == null ? null : Number(row.expires_at),
|
| 67 |
enabled: Boolean(row.enabled),
|
| 68 |
createdAt: Number(row.created_at || 0),
|
|
|
|
| 58 |
|
| 59 |
function toPublicToken(row) {
|
| 60 |
if (!row) return null;
|
| 61 |
+
const encryptedSecret = String(row.encrypted_secret_json || '').trim();
|
| 62 |
return {
|
| 63 |
id: row.id,
|
| 64 |
name: row.name,
|
| 65 |
scopes: JSON.parse(row.scopes_json || '[]'),
|
| 66 |
restrictions: JSON.parse(row.restrictions_json || '{}'),
|
| 67 |
+
recoverable: Boolean(encryptedSecret),
|
| 68 |
expiresAt: row.expires_at == null ? null : Number(row.expires_at),
|
| 69 |
enabled: Boolean(row.enabled),
|
| 70 |
createdAt: Number(row.created_at || 0),
|