feat: implement TASK-WP1-05 account deletion

This commit is contained in:
suyx
2026-07-28 19:07:11 +08:00
parent 03f1509de7
commit 2358f97e5c
20 changed files with 2565 additions and 13 deletions
+189
View File
@@ -3,6 +3,12 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import {
AccountDeletionCompleteRequestSchema,
AccountDeletionResponseSchema,
AccountDeletionSendResponseSchema,
AccountProfileUpdateRequestSchema,
AccountProfileUpdateResponseSchema,
AccountSettingsResponseSchema,
AdminAuthenticatedUserSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
@@ -12,6 +18,7 @@ import {
CorrelationIdSchema,
AuthenticatedUserSchema,
CreditSummarySchema,
CsrfHeadersSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
GenerationErrorCategorySchema,
@@ -37,6 +44,8 @@ import {
type BootstrapResponse,
type AdminLoginCompleteRequest,
type AdminLoginSendRequest,
type AccountDeletionCompleteRequest,
type AccountProfileUpdateRequest,
type LoginCompleteRequest,
type LoginSendRequest,
type RegistrationCompleteRequest,
@@ -226,6 +235,13 @@ export async function createApp(options: CreateAppOptions = {}) {
AdminLoginCompleteResponseSchema,
AdminSessionResponseSchema,
CreditSummarySchema,
CsrfHeadersSchema,
AccountSettingsResponseSchema,
AccountProfileUpdateRequestSchema,
AccountProfileUpdateResponseSchema,
AccountDeletionSendResponseSchema,
AccountDeletionCompleteRequestSchema,
AccountDeletionResponseSchema,
RegistrationSendRequestSchema,
RegistrationSendResponseSchema,
RegistrationCompleteRequestSchema,
@@ -731,6 +747,179 @@ export async function createApp(options: CreateAppOptions = {}) {
},
);
app.get(
"/api/v1/account/settings",
{
schema: {
operationId: "getAccountSettings",
response: {
200: Type.Ref(AccountSettingsResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
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), userSessionCookieName);
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const settings = options.registration.readAccountSettings(token);
const csrfToken = options.registration.issueUserCsrfToken(token);
return {
account: settings.account,
csrf_token: csrfToken,
local_data: {
backup_enabled: settings.localData.backupEnabled,
capacity_status: settings.localData.capacityStatus,
hard_limit_bytes: settings.localData.hardLimitBytes,
location: settings.localData.location,
managed_content_bytes: settings.localData.managedContentBytes,
migration_supported: settings.localData.migrationSupported,
},
profile: {
creator_name: settings.profile.creatorName,
social_id: settings.profile.socialId,
},
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.put(
"/api/v1/account/settings/profile",
{
attachValidation: true,
schema: {
body: Type.Ref(AccountProfileUpdateRequestSchema),
headers: Type.Ref(CsrfHeadersSchema),
operationId: "updateAccountProfile",
response: {
200: Type.Ref(AccountProfileUpdateResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
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 token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token || !csrfToken) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const body = request.body as AccountProfileUpdateRequest;
const saved = options.registration.updateAccountProfile({
creatorName: body.creator_name,
csrfToken,
sessionToken: token,
socialId: body.social_id,
});
return { profile: { creator_name: saved.creatorName, social_id: saved.socialId }, status: saved.status };
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/account/deletion/send",
{
attachValidation: true,
schema: {
headers: Type.Ref(CsrfHeadersSchema),
operationId: "sendAccountDeletionCode",
response: {
200: Type.Ref(AccountDeletionSendResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
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 token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token || !csrfToken) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const sent = await options.registration.sendAccountDeletionCode({ csrfToken, sessionToken: token });
return {
challenge_expires_at: new Date(sent.challengeExpiresAt).toISOString(),
deletion_id: sent.deletionId,
resend_available_at: new Date(sent.resendAvailableAt).toISOString(),
status: sent.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/account/deletion/complete",
{
attachValidation: true,
schema: {
body: Type.Ref(AccountDeletionCompleteRequestSchema),
headers: Type.Ref(LogoutHeadersSchema),
operationId: "completeAccountDeletion",
response: {
200: Type.Ref(AccountDeletionResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
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 token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!token || !csrfToken || !idempotencyKey) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
try {
const body = request.body as AccountDeletionCompleteRequest;
const deleted = options.registration.completeAccountDeletion({
code: body.verification_code,
confirmation: body.confirmation,
csrfToken,
deletionId: body.deletion_id,
idempotencyKey,
sessionToken: token,
});
reply.header("Set-Cookie", `${userSessionCookieName}=; Max-Age=0; Path=/; HttpOnly; SameSite=Strict`);
return deleted;
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/support/check",
{