feat: implement TASK-WP1-04 admin security

This commit is contained in:
suyx
2026-07-28 18:32:50 +08:00
parent 66fe3b763a
commit 03f1509de7
29 changed files with 2344 additions and 40 deletions
+148
View File
@@ -3,6 +3,11 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import {
AdminAuthenticatedUserSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
AdminLoginSendRequestSchema,
AdminSessionResponseSchema,
BootstrapResponseSchema,
CorrelationIdSchema,
AuthenticatedUserSchema,
@@ -30,6 +35,8 @@ import {
createErrorEnvelope,
isCorrelationId,
type BootstrapResponse,
type AdminLoginCompleteRequest,
type AdminLoginSendRequest,
type LoginCompleteRequest,
type LoginSendRequest,
type RegistrationCompleteRequest,
@@ -101,6 +108,8 @@ const contentSecurityPolicy = [
].join("; ");
const authFlowCookieName = "dada_auth_flow";
const userSessionCookieName = "dada_session";
const adminAuthFlowCookieName = "dada_admin_auth_flow";
const adminSessionCookieName = "dada_admin_session";
function requestCorrelationId(headers: Record<string, string | string[] | undefined>) {
const header = headers["x-correlation-id"];
@@ -211,6 +220,11 @@ export async function createApp(options: CreateAppOptions = {}) {
ErrorDetailsSchema,
ErrorEnvelopeSchema,
AuthenticatedUserSchema,
AdminAuthenticatedUserSchema,
AdminLoginSendRequestSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
AdminSessionResponseSchema,
CreditSummarySchema,
RegistrationSendRequestSchema,
RegistrationSendResponseSchema,
@@ -302,6 +316,140 @@ export async function createApp(options: CreateAppOptions = {}) {
},
);
app.post(
"/api/v1/admin-auth/login/send",
{
attachValidation: true,
schema: {
body: Type.Ref(AdminLoginSendRequestSchema),
operationId: "sendAdminLoginCode",
response: {
200: Type.Ref(RegistrationSendResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const existingFlow = cookieValue(headerValue(request.headers.cookie), adminAuthFlowCookieName);
const clientKey = existingFlow ?? randomBytes(32).toString("base64url");
try {
const body = request.body as AdminLoginSendRequest;
const result = await options.registration.sendAdminLoginCode({ clientKey, email: body.email });
if (!existingFlow) {
reply.header(
"Set-Cookie",
`${adminAuthFlowCookieName}=${clientKey}; Max-Age=${10 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
}
return {
challenge_expires_at: new Date(result.challengeExpiresAt).toISOString(),
registration_id: result.registrationId,
resend_available_at: new Date(result.resendAvailableAt).toISOString(),
status: result.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/admin-auth/login/complete",
{
attachValidation: true,
schema: {
body: Type.Ref(AdminLoginCompleteRequestSchema),
headers: Type.Ref(RegistrationCompleteHeadersSchema),
operationId: "completeAdminLogin",
response: {
200: Type.Ref(AdminLoginCompleteResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const clientKey = cookieValue(headerValue(request.headers.cookie), adminAuthFlowCookieName);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!clientKey || !idempotencyKey) return registrationValidationFailure(reply, request.id);
try {
const body = request.body as AdminLoginCompleteRequest;
const result = options.registration.completeAdminLogin({
clientKey,
code: body.verification_code,
idempotencyKey,
registrationId: body.registration_id,
});
reply.header(
"Set-Cookie",
`${adminSessionCookieName}=${result.sessionToken}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
return {
admin: {
role: result.admin.role,
status: result.admin.status,
user_id: result.admin.userId,
},
audience: result.audience,
session_expires_at: new Date(result.sessionExpiresAt).toISOString(),
status: result.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/admin-auth/session",
{
schema: {
operationId: "getAdminSession",
response: {
200: Type.Ref(AdminSessionResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Authentication"],
},
},
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const session = token ? options.registration.readAdminSession(token) : undefined;
if (!session) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
return {
acknowledged_private_content_notice_version: null,
admin: { role: "super_admin" as const, status: "active" as const, user_id: session.user_id },
audience: "admin" as const,
authenticated: true as const,
csrf_token: options.registration.issueAdminCsrfToken(token!),
current_private_content_notice_version: null,
expires_at: new Date(session.expires_at).toISOString(),
notice_acknowledged: false,
};
},
);
app.post(
"/api/v1/auth/login/send",
{