fix(portal): 管理員忘記密碼自救援出口(arcrun-rag#25)
唯一 admin 忘記 portal 密碼就永久卡死:requirePortalAdmin 系列端點全部要 先有 portal session 才進得去,bootstrap 又只能跑一次——登入頁只會叫他 「聯絡管理員」,而他自己就是管理員,沒有下一步。 新增 POST /portal/admin/recover-password,複用 bootstrap 已在用的 console owner session 當人閘(與 portal 密碼完全獨立存放的另一組帳密)。畫面入口: /console → 設定 → 「Portal 帳號密碼救援」;/portal 登入頁加一行連結指過去。 本機真瀏覽器 E2E 驗證(wrangler dev 18787/18788 + 本機靜態伺服,真的走一輪 forgot-password 狀態):first-time setup 建帳號 → 故意打錯密碼確認鎖死 (email 或密碼錯誤)→ 點連結進 /console → 用 console 密碼登入 → 設定頁輸入 portal email → 產生新密碼 BPq2Rs4p7dBWMd6d → 回 /portal 用新密碼登入成功。 cypher-executor 4 個新測試 + 既有 59/60 綠(唯一失敗是既有 pre-existing /portal HTML 殼 404,與本次無關,git stash 驗證過)。
This commit is contained in:
@@ -263,6 +263,70 @@ describe('POST /portal/admin/users/:id/reset-password', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 3.5 recover-password(arcrun-rag#25:admin 忘記 portal 密碼自救)═══════════════
|
||||
|
||||
describe('POST /portal/admin/recover-password', () => {
|
||||
it('無 console owner session → 401,不碰 KBDB', async () => {
|
||||
const res = await json('POST', '/portal/admin/recover-password', { email: 'admin@example.com' });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('有 console session 但 email 格式不對 → 400,不碰 KBDB', async () => {
|
||||
await env.SESSIONS_KV.put('console_sess:owner-token', JSON.stringify({ created_at: Date.now() }));
|
||||
const res = await json(
|
||||
'POST',
|
||||
'/portal/admin/recover-password',
|
||||
{ email: 'not-an-email' },
|
||||
{ Authorization: 'Bearer owner-token' },
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('查無此 email 的 portal 帳號 → 404,不誤導成別種錯誤', async () => {
|
||||
await env.SESSIONS_KV.put('console_sess:owner-token', JSON.stringify({ created_at: Date.now() }));
|
||||
mockHeadLookup('ghost@example.com', null);
|
||||
const res = await json(
|
||||
'POST',
|
||||
'/portal/admin/recover-password',
|
||||
{ email: 'ghost@example.com' },
|
||||
{ Authorization: 'Bearer owner-token' },
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('console session 有效+帳號存在 → 回一次性新密碼;PATCH 落 KBDB 的是新 hash 非明碼;**不需要任何 portal session**', async () => {
|
||||
await env.SESSIONS_KV.put('console_sess:owner-token', JSON.stringify({ created_at: Date.now() }));
|
||||
// 刻意不 seedAdminSession():這條路唯一該吃的是 console session,機械證明繞得過
|
||||
// 「忘記 portal 密碼 ⇒ 沒有 portal_sess ⇒ 打不進其他 admin 端點」這個死結。
|
||||
mockHeadLookup('admin@example.com', 'rec_admin');
|
||||
mockGetRecord('rec_admin', adminValues());
|
||||
let patched = '';
|
||||
fetchMock
|
||||
.get(KBDB)
|
||||
.intercept({ path: '/records/rec_admin', method: 'PATCH' })
|
||||
.reply(200, (opts) => {
|
||||
patched = String(opts.body);
|
||||
return { success: true, record: { record_id: 'rec_admin', template_id: 'tpl_pu', values: adminValues() } };
|
||||
});
|
||||
const res = await json(
|
||||
'POST',
|
||||
'/portal/admin/recover-password',
|
||||
{ email: 'Admin@Example.com' }, // 混寫大小寫,驗證正規化成小寫再查
|
||||
{ Authorization: 'Bearer owner-token' },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const data = (await res.json()) as { success: boolean; email: string; password: string };
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.email).toBe('admin@example.com');
|
||||
expect(typeof data.password).toBe('string');
|
||||
expect(data.password.length).toBe(16);
|
||||
expect(patched).not.toContain(data.password); // 明碼不落 KBDB
|
||||
const sent = JSON.parse(patched) as { values: Record<string, string> };
|
||||
expect(sent.values.password_hash.startsWith(`pbkdf2-sha256$${PBKDF2_ITERATIONS}$`)).toBe(true);
|
||||
expect(sent.values.password_hash).not.toBe(storedHash); // 真的換了
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 4. 庫權限勾選(libraries PATCH)═══════════════
|
||||
|
||||
describe('PATCH libraries(每帳號可查庫)', () => {
|
||||
|
||||
Reference in New Issue
Block a user