feat: implement TASK-WP1-05 account deletion
This commit is contained in:
@@ -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",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user