merge: integrate secure remembered password

This commit is contained in:
2026-08-20 23:41:48 +08:00
11 changed files with 530 additions and 17 deletions

View File

@@ -23,10 +23,15 @@ import {
import { logger } from '../../utils/logger';
import type { WorksSquareTokenPayload } from '../../services/works-square-session';
import { normalizeModuleAccess } from '../../../shared/module-access';
import {
getRememberedPasswordState,
updateRememberedPassword,
} from '../../services/remembered-password';
type PasswordLoginInput = {
username?: unknown;
password?: unknown;
rememberPassword?: unknown;
};
type MobileLoginInput = {
@@ -219,6 +224,7 @@ function sendUpstreamFailure(
async function commitLoginPayload(
res: ServerResponse,
payload: unknown,
onCommitted?: () => Promise<void>,
): Promise<void> {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
sendJson(res, 502, { success: false, error: '登录服务返回了无效响应,请稍后重试。' });
@@ -228,6 +234,7 @@ async function commitLoginPayload(
const session = await commitWorksSquareSessionFromTokenPayload(
payload as WorksSquareTokenPayload,
);
await onCommitted?.();
sendJson(res, 200, {
success: true,
token: withoutRefreshToken(payload),
@@ -246,10 +253,11 @@ async function handlePasswordLogin(
if (!await ensureRuntimeReadyForLogin(res, ctx)) return;
const body = readExactJsonObject(
await parseJsonBody<PasswordLoginInput>(req),
['username', 'password'],
['username', 'password', 'rememberPassword'],
);
const username = readRequiredString(body.username, 'username');
const password = readRequiredString(body.password, 'password');
const rememberPassword = readOptionalBoolean(body.rememberPassword, false);
const result = await fetchWorks('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -265,7 +273,17 @@ async function handlePasswordLogin(
);
return;
}
await commitLoginPayload(res, result.payload);
await commitLoginPayload(res, result.payload, async () => {
await updateRememberedPassword(
rememberPassword ? { username, password } : null,
);
});
}
async function handleRememberedPassword(res: ServerResponse): Promise<void> {
const state = await getRememberedPasswordState();
res.setHeader('Cache-Control', 'no-store');
sendJson(res, 200, { success: true, ...state });
}
async function handleMobileLogin(
@@ -717,6 +735,11 @@ export async function handleAuthRoutes(
return true;
}
if (url.pathname === '/api/auth/remembered-password' && req.method === 'GET') {
await handleRememberedPassword(res);
return true;
}
if (url.pathname === '/api/auth/mobile-login' && req.method === 'POST') {
await handleMobileLogin(req, res, ctx);
return true;