Files
makelore/electron/api/routes/auth.ts

859 lines
27 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from 'http';
import type { HostApiContext } from '../context';
import { parseJsonBody, sendJson } from '../route-utils';
import { proxyAwareFetch } from '../../utils/proxy-fetch';
import { WORKS_SQUARE_CONFIG } from '../works-config';
import {
clearWorksSquareSession,
commitWorksSquareSession,
commitWorksSquareSessionFromTokenPayload,
discardUnrestorableWorksSquareSession,
flushWorksSquareSessionPersistence,
getValidWorksSquareAccessToken,
getWorksSquareSessionRestoreStatus,
getWorksSquareSessionSnapshot,
markWorksSquareSessionActive,
retryWorksSquareSessionRestore,
} from '../../services/works-square-session';
import {
clearManagedWorksSquareRuntime,
clearManagedWorksSquareRuntimeBestEffort,
ensureManagedWorksSquareRuntimeClean,
} from '../../services/works-square-runtime';
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 = {
phone?: unknown;
code?: unknown;
};
type MobileCodeInput = {
phone?: unknown;
imageRandomStr?: unknown;
imageCode?: unknown;
};
type LogoutInput = {
accessToken?: unknown;
};
type SessionSyncInput = {
accessToken?: unknown;
refreshToken?: unknown;
tokenType?: unknown;
expiresAt?: unknown;
lastActiveAt?: unknown;
};
type SessionRefreshInput = {
forceRefresh?: unknown;
};
const MAX_AUTH_ERROR_LENGTH = 180;
const MAX_CAPTCHA_IMAGE_BYTES = 1024 * 1024;
const MAX_PUBLIC_USERNAME_LENGTH = 256;
const MAX_PUBLIC_ID_LENGTH = 128;
const MAX_PUBLIC_AUTHORITIES = 100;
const MAX_PUBLIC_AUTHORITY_LENGTH = 128;
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
function readRequiredString(value: unknown, field: string): string {
if (typeof value !== 'string' || !value.trim()) {
throw new Error(`Missing ${field}`);
}
return value.trim();
}
function readOptionalTrimmedString(value: unknown): string | null {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function readOptionalNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function readOptionalBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === 'boolean' ? value : fallback;
}
function readBoundedString(value: unknown, maxLength: number): string | null {
const normalized = readOptionalTrimmedString(value);
return normalized && normalized.length <= maxLength ? normalized : null;
}
function readPublicStringId(...values: unknown[]): string | null {
for (const value of values) {
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
const normalized = readBoundedString(value, MAX_PUBLIC_ID_LENGTH);
if (normalized) return normalized;
}
return null;
}
function readPublicStringOrNumberId(...values: unknown[]): string | number | null {
for (const value of values) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
const normalized = readBoundedString(value, MAX_PUBLIC_ID_LENGTH);
if (normalized) return normalized;
}
return null;
}
function readPublicAuthorities(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
.map((authority) => readBoundedString(authority, MAX_PUBLIC_AUTHORITY_LENGTH))
.filter((authority): authority is string => authority !== null)
.slice(0, MAX_PUBLIC_AUTHORITIES);
}
function projectCurrentUser(profile: Record<string, unknown>): {
username: string;
userId: string | null;
tenantId: string | number | null;
deptId: string | number | null;
authorities: string[];
} | null {
const username = readBoundedString(profile.username, MAX_PUBLIC_USERNAME_LENGTH)
?? readBoundedString(profile.user_name, MAX_PUBLIC_USERNAME_LENGTH);
if (!username) return null;
return {
username,
userId: readPublicStringId(profile.user_id, profile.userId, profile.id),
tenantId: readPublicStringOrNumberId(profile.tenant_id, profile.tenantId),
deptId: readPublicStringOrNumberId(profile.dept_id, profile.deptId),
authorities: readPublicAuthorities(profile.authorities),
};
}
function withoutRefreshToken(payload: unknown): unknown {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
const { refresh_token: _refreshToken, ...publicPayload } = payload as Record<string, unknown>;
return publicPayload;
}
function normalizeWorksBase(value = WORKS_SQUARE_CONFIG.apiBaseUrl): string {
const apiBase = value.replace(/\/+$/, '');
if (!/^https?:\/\//i.test(apiBase)) {
throw new Error('Works Square API base URL must start with http:// or https://');
}
return apiBase;
}
function createWorksUrl(pathname: string): URL {
return new URL(`${normalizeWorksBase()}${pathname}`);
}
async function readResponsePayload(response: Response): Promise<unknown> {
const text = await response.text();
if (!text.trim()) return null;
try {
return JSON.parse(text) as unknown;
} catch {
return text;
}
}
function getErrorMessage(payload: unknown, fallback: string): string {
const compact = (value: string): string => {
const normalized = value.replace(/\s+/g, ' ').trim();
if (
!normalized
|| normalized.length > MAX_AUTH_ERROR_LENGTH
|| /<!doctype\b|<html\b|<head\b|<body\b/i.test(normalized)
) {
return fallback;
}
return normalized;
};
if (payload && typeof payload === 'object') {
const record = payload as Record<string, unknown>;
for (const field of ['detail', 'msg', 'message', 'error_description', 'error']) {
const value = record[field];
if (typeof value === 'string' && value.trim()) {
return compact(value);
}
}
}
if (typeof payload === 'string' && payload.trim()) {
return compact(payload);
}
return fallback;
}
function readExactJsonObject(
value: unknown,
allowedFields: readonly string[],
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Request body must be a JSON object');
}
const record = value as Record<string, unknown>;
const unexpectedField = Object.keys(record).find((field) => !allowedFields.includes(field));
if (unexpectedField) {
throw new Error(`Unexpected field: ${unexpectedField}`);
}
return record;
}
function isFailureEnvelope(payload: unknown): boolean {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return false;
const record = payload as Record<string, unknown>;
if (record.success === false || record.data === false) return true;
if (typeof record.code === 'number') return record.code !== 0;
if (typeof record.code === 'string' && record.code.trim()) return record.code.trim() !== '0';
return false;
}
async function fetchWorks(
pathname: string,
init: RequestInit,
): Promise<{ response: Response; payload: unknown } | null> {
try {
const response = await proxyAwareFetch(createWorksUrl(pathname).toString(), init);
return { response, payload: await readResponsePayload(response) };
} catch {
return null;
}
}
async function readBoundedResponseBytes(response: Response, maxBytes: number): Promise<Buffer | null> {
if (!response.body) return Buffer.alloc(0);
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let totalBytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
if (totalBytes > maxBytes) {
await reader.cancel();
return null;
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
return Buffer.concat(chunks, totalBytes);
}
function sendUpstreamFailure(
res: ServerResponse,
response: Response | null,
payload: unknown,
serviceFailureMessage: string,
): void {
const isClientError = response !== null && (
response.ok || (response.status >= 400 && response.status < 500)
);
sendJson(res, response?.ok ? 400 : (isClientError ? (response?.status ?? 400) : 502), {
success: false,
error: isClientError
? getErrorMessage(payload, '请求未通过,请检查后重试。')
: serviceFailureMessage,
});
}
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: '登录服务返回了无效响应,请稍后重试。' });
return;
}
try {
const session = await commitWorksSquareSessionFromTokenPayload(
payload as WorksSquareTokenPayload,
);
await onCommitted?.();
sendJson(res, 200, {
success: true,
token: withoutRefreshToken(payload),
session,
});
} catch {
sendJson(res, 503, { success: false, error: '登录状态暂时无法保存,请稍后重试。' });
}
}
async function handlePasswordLogin(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureRuntimeReadyForLogin(res, ctx)) return;
const body = readExactJsonObject(
await parseJsonBody<PasswordLoginInput>(req),
['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' },
body: JSON.stringify({ username, password }),
});
if (!result || !result.response.ok || isFailureEnvelope(result.payload)) {
sendUpstreamFailure(
res,
result?.response ?? null,
result?.payload,
'登录服务暂时不可用,请稍后重试。',
);
return;
}
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(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureRuntimeReadyForLogin(res, ctx)) return;
const body = readExactJsonObject(
await parseJsonBody<MobileLoginInput>(req),
['phone', 'code'],
);
const phone = readRequiredString(body.phone, 'phone');
const code = readRequiredString(body.code, 'code');
const result = await fetchWorks('/api/auth/mobile-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code }),
});
if (!result || !result.response.ok || isFailureEnvelope(result.payload)) {
sendUpstreamFailure(
res,
result?.response ?? null,
result?.payload,
'登录服务暂时不可用,请稍后重试。',
);
return;
}
await commitLoginPayload(res, result.payload);
}
async function handleMobileCode(req: IncomingMessage, res: ServerResponse): Promise<void> {
const body = readExactJsonObject(
await parseJsonBody<MobileCodeInput>(req),
['phone', 'imageRandomStr', 'imageCode'],
);
const phone = readRequiredString(body.phone, 'phone');
const imageRandomStr = readRequiredString(body.imageRandomStr, 'imageRandomStr');
const imageCode = readRequiredString(body.imageCode, 'imageCode');
if (!UUID_PATTERN.test(imageRandomStr)) {
throw new Error('imageRandomStr must be a UUID');
}
const result = await fetchWorks('/api/auth/mobile-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, imageRandomStr, imageCode }),
});
if (
!result
|| !result.response.ok
|| !result.payload
|| typeof result.payload !== 'object'
|| Array.isArray(result.payload)
|| isFailureEnvelope(result.payload)
) {
sendUpstreamFailure(
res,
result?.response ?? null,
result?.payload,
'短信验证码服务暂时不可用,请稍后重试。',
);
return;
}
sendJson(res, 200, { success: true });
}
async function handleMobileImageCode(url: URL, res: ServerResponse): Promise<void> {
res.setHeader('Cache-Control', 'no-store');
const randomStr = readRequiredString(url.searchParams.get('randomStr'), 'randomStr');
if (!UUID_PATTERN.test(randomStr)) {
throw new Error('randomStr must be a UUID');
}
if (
url.searchParams.getAll('randomStr').length !== 1
|| [...url.searchParams.keys()].some((key) => key !== 'randomStr')
) {
throw new Error('Unexpected query parameter');
}
let response: Response;
try {
const upstreamUrl = createWorksUrl('/api/auth/mobile-image-code');
upstreamUrl.searchParams.set('randomStr', randomStr);
response = await proxyAwareFetch(upstreamUrl.toString(), { method: 'GET' });
} catch {
sendJson(res, 502, {
success: false,
error: '图形验证码服务暂时不可用,请稍后重试。',
});
return;
}
if (!response.ok) {
const payload = await readResponsePayload(response);
sendUpstreamFailure(res, response, payload, '图形验证码服务暂时不可用,请稍后重试。');
return;
}
const contentType = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase();
const contentLength = Number(response.headers.get('content-length'));
if (
contentType !== 'image/png'
|| (Number.isFinite(contentLength) && contentLength > MAX_CAPTCHA_IMAGE_BYTES)
) {
sendJson(res, 502, {
success: false,
error: '图形验证码服务返回了无效图片,请稍后重试。',
});
return;
}
const bytes = await readBoundedResponseBytes(response, MAX_CAPTCHA_IMAGE_BYTES);
if (
!bytes
|| bytes.length < PNG_MAGIC.length
|| !bytes.subarray(0, PNG_MAGIC.length).equals(PNG_MAGIC)
) {
sendJson(res, 502, {
success: false,
error: '图形验证码服务返回了无效图片,请稍后重试。',
});
return;
}
sendJson(res, 200, {
success: true,
image: {
mimeType: 'image/png',
dataBase64: bytes.toString('base64'),
},
});
}
function projectPublicUrl(value: unknown): string | null {
if (typeof value !== 'string' || !value.trim()) return null;
try {
const worksBase = new URL(`${normalizeWorksBase()}/`);
const projected = new URL(value.trim(), worksBase);
if (projected.username || projected.password) return null;
if (projected.origin !== worksBase.origin && projected.protocol !== 'https:') return null;
if (projected.origin === worksBase.origin && !['http:', 'https:'].includes(projected.protocol)) {
return null;
}
return projected.toString();
} catch {
return null;
}
}
async function handlePublicConfig(res: ServerResponse): Promise<void> {
const result = await fetchWorks('/api/app-config', { method: 'GET' });
if (!result || !result.response.ok) {
sendUpstreamFailure(
res,
result?.response ?? null,
result?.payload,
'登录配置服务暂时不可用,请稍后重试。',
);
return;
}
const config = result.payload && typeof result.payload === 'object' && !Array.isArray(result.payload)
? result.payload as Record<string, unknown>
: {};
const legal = config.legal && typeof config.legal === 'object' && !Array.isArray(config.legal)
? config.legal as Record<string, unknown>
: {};
const auth = config.auth && typeof config.auth === 'object' && !Array.isArray(config.auth)
? config.auth as Record<string, unknown>
: {};
res.setHeader('Cache-Control', 'no-store');
sendJson(res, 200, {
success: true,
links: {
termsUrl: projectPublicUrl(legal.terms_url),
privacyUrl: projectPublicUrl(legal.privacy_url),
forgotPasswordUrl: projectPublicUrl(auth.forgot_password_url),
},
});
}
async function handleSessionSync(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureWorksSquareSessionRestored()) {
sendJson(res, 503, { success: false, error: '登录状态暂时无法恢复,请稍后重试。' });
return;
}
try {
await flushWorksSquareSessionPersistence();
} catch (error) {
logger.warn('[auth] Session persistence is temporarily unavailable during sync', error);
sendJson(res, 503, { success: false, error: '登录状态暂时无法同步,请稍后重试。' });
return;
}
const body = await parseJsonBody<SessionSyncInput>(req);
let session = getWorksSquareSessionSnapshot();
const legacyAccessToken = readOptionalTrimmedString(body.accessToken);
if (!session && legacyAccessToken) {
try {
await ensureManagedWorksSquareRuntimeClean(ctx);
session = await commitWorksSquareSession({
accessToken: legacyAccessToken,
refreshToken: readOptionalTrimmedString(body.refreshToken),
tokenType: readOptionalTrimmedString(body.tokenType),
expiresAt: readOptionalNumber(body.expiresAt),
lastActiveAt: readOptionalNumber(body.lastActiveAt),
});
} catch (error) {
logger.warn('[auth] Failed to persist a migrated Renderer session', error);
sendJson(res, 503, { success: false, error: '登录状态暂时无法同步,请稍后重试。' });
return;
}
}
if (!session) {
sendJson(res, 200, { success: true, session: null });
return;
}
sendJson(res, 200, { success: true, session });
}
async function handleSessionRefresh(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureWorksSquareSessionRestored()) {
sendJson(res, 503, { success: false, error: '登录状态暂时无法恢复,请稍后重试。' });
return;
}
const body = await parseJsonBody<SessionRefreshInput>(req);
try {
const accessToken = await getValidWorksSquareAccessToken({
forceRefresh: readOptionalBoolean(body.forceRefresh, true),
});
const session = getWorksSquareSessionSnapshot();
if (accessToken && session) {
sendJson(res, 200, { success: true, session });
return;
}
if (!session) {
await clearManagedWorksSquareRuntimeBestEffort(ctx, 'terminal session refresh');
sendJson(res, 401, { success: false, error: '登录已过期,请重新授权。' });
return;
}
sendJson(res, 502, { success: false, error: '登录续期暂时失败,请稍后重试。' });
} catch (error) {
logger.warn('[auth] Failed to refresh the managed Works Square session', error);
sendJson(res, 502, { success: false, error: '登录续期暂时失败,请稍后重试。' });
}
}
async function handleSessionActivity(
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureWorksSquareSessionRestored()) {
sendJson(res, 503, { success: false, error: '登录状态暂时无法恢复,请稍后重试。' });
return;
}
let session;
try {
session = await markWorksSquareSessionActive();
} catch (error) {
logger.warn('[auth] Failed to persist Works Square session activity', error);
sendJson(res, 503, { success: false, error: '登录活动暂时无法保存,请稍后重试。' });
return;
}
if (!session) {
await clearManagedWorksSquareRuntimeBestEffort(ctx, 'idle session activity');
sendJson(res, 401, { success: false, error: '登录已过期,请重新授权。' });
return;
}
sendJson(res, 200, {
success: true,
session,
});
}
async function handleCurrentUser(res: ServerResponse, ctx: HostApiContext): Promise<void> {
const accessToken = await getValidWorksSquareAccessToken({ forceRefresh: false });
if (!accessToken) {
sendJson(res, 401, { success: false, error: '登录已过期,请重新授权。' });
return;
}
const response = await proxyAwareFetch(createWorksUrl('/api/auth/me').toString(), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const payload = await readResponsePayload(response);
if (!response.ok) {
if (response.status === 401) {
clearWorksSquareSession();
await Promise.allSettled([
flushWorksSquareSessionPersistence(),
clearManagedWorksSquareRuntimeBestEffort(ctx, 'terminal current-user lookup'),
]);
}
sendJson(res, response.status === 401 ? 401 : 502, {
success: false,
error: response.status === 401
? '登录已过期,请重新授权。'
: '暂时无法读取模块权限,请稍后重试。',
});
return;
}
const profile = payload && typeof payload === 'object' && !Array.isArray(payload)
? payload as Record<string, unknown>
: {};
sendJson(res, 200, {
success: true,
user: projectCurrentUser(profile),
moduleAccess: normalizeModuleAccess(profile.module_access),
});
}
async function ensureWorksSquareSessionRestored(): Promise<boolean> {
if (getWorksSquareSessionRestoreStatus() === 'unavailable') {
await retryWorksSquareSessionRestore();
}
return getWorksSquareSessionRestoreStatus() === 'ready';
}
async function ensureRuntimeReadyForLogin(
res: ServerResponse,
ctx: HostApiContext,
): Promise<boolean> {
let discardedUnrestorableSession = false;
if (!await ensureWorksSquareSessionRestored()) {
if (!await discardUnrestorableWorksSquareSession()) {
sendJson(res, 503, {
success: false,
error: '登录状态暂时无法恢复,请稍后重试。',
});
return false;
}
discardedUnrestorableSession = true;
}
try {
if (discardedUnrestorableSession) {
await clearManagedWorksSquareRuntime(ctx, undefined, true);
}
await ensureManagedWorksSquareRuntimeClean(ctx);
return true;
} catch (error) {
logger.error('[auth] Failed to clean the previous runtime before login', error);
sendJson(res, 503, {
success: false,
error: '上次登录的本地运行环境尚未清理完成,请稍后重试。',
});
return false;
}
}
async function handleSessionClear(res: ServerResponse, ctx: HostApiContext): Promise<void> {
const accessToken = getWorksSquareSessionSnapshot()?.accessToken;
clearWorksSquareSession();
await Promise.all([
flushWorksSquareSessionPersistence(),
clearManagedWorksSquareRuntime(ctx, accessToken),
]);
sendJson(res, 200, { success: true });
}
async function handleLogout(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
const body = readExactJsonObject(
await parseJsonBody<LogoutInput>(req),
['accessToken'],
);
const rendererAccessToken = readOptionalTrimmedString(body.accessToken);
const accessToken = getWorksSquareSessionSnapshot()?.accessToken
?? readRequiredString(rendererAccessToken, 'accessToken');
let cleanupError: Error | null = null;
clearWorksSquareSession();
try {
await clearManagedWorksSquareRuntime(ctx, accessToken);
} catch (error) {
cleanupError = error instanceof Error ? error : new Error(String(error));
logger.error('[auth] Failed to clear managed Works Square runtime state during logout', cleanupError);
}
try {
await flushWorksSquareSessionPersistence();
} catch (error) {
cleanupError = error instanceof Error ? error : new Error(String(error));
logger.error('[auth] Failed to persist the local session clear during logout', cleanupError);
}
let response: Response;
try {
response = await proxyAwareFetch(createWorksUrl('/api/auth/logout').toString(), {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
} catch (error) {
if (cleanupError) {
sendJson(res, 500, {
success: false,
error: 'Failed to clear local AI runtime state',
});
return;
}
throw error;
}
if (cleanupError) {
sendJson(res, 500, {
success: false,
error: 'Failed to clear local AI runtime state',
});
return;
}
if (!response.ok) {
const payload = await readResponsePayload(response);
sendJson(res, response.status === 401 ? 401 : 502, {
success: false,
error: getErrorMessage(payload, `Logout failed (${response.status})`),
});
return;
}
sendJson(res, 200, { success: true });
}
export async function handleAuthRoutes(
req: IncomingMessage,
res: ServerResponse,
url: URL,
ctx: HostApiContext,
): Promise<boolean> {
if (!url.pathname.startsWith('/api/auth')) {
return false;
}
try {
if (url.pathname === '/api/auth/login' && req.method === 'POST') {
await handlePasswordLogin(req, res, ctx);
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;
}
if (url.pathname === '/api/auth/mobile-code' && req.method === 'POST') {
await handleMobileCode(req, res);
return true;
}
if (url.pathname === '/api/auth/mobile-image-code' && req.method === 'GET') {
await handleMobileImageCode(url, res);
return true;
}
if (url.pathname === '/api/auth/public-config' && req.method === 'GET') {
await handlePublicConfig(res);
return true;
}
if (url.pathname === '/api/auth/session/sync' && req.method === 'POST') {
await handleSessionSync(req, res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/refresh' && req.method === 'POST') {
await handleSessionRefresh(req, res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/activity' && req.method === 'POST') {
await handleSessionActivity(res, ctx);
return true;
}
if (url.pathname === '/api/auth/me' && req.method === 'GET') {
await handleCurrentUser(res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/clear' && req.method === 'POST') {
await handleSessionClear(res, ctx);
return true;
}
if (url.pathname === '/api/auth/logout' && req.method === 'POST') {
await handleLogout(req, res, ctx);
return true;
}
sendJson(res, 404, { success: false, error: `No route for ${req.method} ${url.pathname}` });
return true;
} catch (error) {
sendJson(res, 400, {
success: false,
error: error instanceof Error ? error.message : String(error),
});
return true;
}
}