diff --git a/lib/server/auth-helpers.ts b/lib/server/auth-helpers.ts index 4bcd687..9a2ded9 100644 --- a/lib/server/auth-helpers.ts +++ b/lib/server/auth-helpers.ts @@ -177,6 +177,14 @@ export async function verifyPassword(password: string, salt: string, expectedHas return actual.hash === expectedHash; } +export function isBootstrapAdminCredential( + username: string, + password: string, + adminPassword: string +): boolean { + return normalizeUsername(username) === 'admin' && !!adminPassword && password === adminPassword; +} + export async function signSessionPayload(payload: SessionPayload, secret: string): Promise { const payloadBytes = encodeText(JSON.stringify(payload)); const encodedPayload = encodeBase64Url(payloadBytes); diff --git a/lib/server/auth.ts b/lib/server/auth.ts index 4b36df1..e7004d3 100644 --- a/lib/server/auth.ts +++ b/lib/server/auth.ts @@ -5,6 +5,7 @@ import { createStoredAccount, ensureUniqueUsername, hashPassword, + isBootstrapAdminCredential, normalizeUsername, parseBootstrapAccounts, resolveLoginMode, @@ -350,11 +351,41 @@ async function authenticateManagedLogin(username: string, password: string): Pro if (!normalizedUsername || !password) return null; const accounts = await ensureManagedAccountsBootstrapped(); - const account = accounts.find((item) => item.username === normalizedUsername); - if (!account) return null; + let account = accounts.find((item) => item.username === normalizedUsername); + const usesBootstrapAdminCredential = isBootstrapAdminCredential( + normalizedUsername, + password, + effectiveAdminPassword + ); - const valid = await verifyPassword(password, account.passwordSalt, account.passwordHash); - if (!valid) return null; + if (!account) { + if (!usesBootstrapAdminCredential) return null; + + account = await createStoredAccount({ + username: 'admin', + password, + name: '超级管理员', + role: 'super_admin', + customPermissions: [], + }); + await saveManagedAccounts([...accounts, account]); + } else { + const valid = await verifyPassword(password, account.passwordSalt, account.passwordHash); + if (!valid) { + if (!usesBootstrapAdminCredential) return null; + + const nextPassword = await hashPassword(password); + account = { + ...account, + passwordHash: nextPassword.hash, + passwordSalt: nextPassword.salt, + updatedAt: Date.now(), + }; + await saveManagedAccounts( + accounts.map((item) => item.id === account?.id ? account : item) + ); + } + } return { accountId: account.id, diff --git a/tests/auth.test.ts b/tests/auth.test.ts index 983d15c..65be611 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -3,6 +3,7 @@ import assert from 'node:assert/strict'; import { createStoredAccount, hashPassword, + isBootstrapAdminCredential, parseBootstrapAccounts, resolveLoginMode, shouldUseSecureSessionCookie, @@ -73,6 +74,13 @@ test('hashPassword and verifyPassword round-trip correctly', async () => { assert.equal(await verifyPassword('wrong-password', password.salt, password.hash), false); }); +test('bootstrap admin credential only accepts the configured admin password', () => { + assert.equal(isBootstrapAdminCredential('ADMIN', 'current-secret', 'current-secret'), true); + assert.equal(isBootstrapAdminCredential('admin', 'old-secret', 'current-secret'), false); + assert.equal(isBootstrapAdminCredential('viewer', 'current-secret', 'current-secret'), false); + assert.equal(isBootstrapAdminCredential('admin', '', ''), false); +}); + test('signSessionPayload and verifySessionToken reject tampering', async () => { const token = await signSessionPayload({ accountId: 'account-1',