feat: complete TASK-WP2-03 project lifecycle

This commit is contained in:
suyx
2026-08-02 20:00:18 +08:00
parent da6fa25e60
commit 9b4860cfc2
17 changed files with 1477 additions and 10 deletions
+67
View File
@@ -45,6 +45,9 @@ import {
ProjectRatioSchema,
ProjectRenameRequestSchema,
ProjectRenameResponseSchema,
ProjectRestoreResponseSchema,
ProjectPurgeResponseSchema,
ProjectTrashResponseSchema,
ProjectStateConflictResponseSchema,
ProjectStateSaveHeadersSchema,
ProjectStateSaveResponseSchema,
@@ -370,6 +373,9 @@ export async function createApp(options: CreateAppOptions = {}) {
ProjectEditableStateSchema,
ProjectRenameRequestSchema,
ProjectRenameResponseSchema,
ProjectTrashResponseSchema,
ProjectRestoreResponseSchema,
ProjectPurgeResponseSchema,
ProjectStateSaveHeadersSchema,
ProjectStateSaveResponseSchema,
ProjectStateConflictResponseSchema,
@@ -1247,6 +1253,67 @@ export async function createApp(options: CreateAppOptions = {}) {
},
);
const projectLifecycle = (
action: "trash" | "restore" | "purge",
operationId: string,
responseSchema: typeof ProjectTrashResponseSchema | typeof ProjectRestoreResponseSchema | typeof ProjectPurgeResponseSchema,
) => {
app.post(
`/api/v1/projects/:projectId/${action}`,
{
attachValidation: true,
schema: {
headers: Type.Ref(CsrfHeadersSchema),
operationId,
params: Type.Ref(ProjectParamsSchema),
response: {
200: Type.Ref(responseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
409: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.projects) {
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 owner = options.registration.authorizeUserMutation({ csrfToken, sessionToken: token });
const projectId = (request.params as ProjectParams).projectId;
if (action === "trash") {
const project = options.projects.trashProject(owner.userId, projectId);
return { deleted_at: project.deletedAt, project_id: projectId, purge_at: project.purgeAt, status: "trashed" as const };
}
if (action === "restore") {
options.projects.restoreProject(owner.userId, projectId);
return { deleted_at: null, project_id: projectId, purge_at: null, status: "active" as const };
}
options.projects.purgeProject(owner.userId, projectId);
return { project_id: projectId, status: "purged" as const };
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: projectFailure(reply, request.id, error);
}
},
);
};
projectLifecycle("trash", "trashProject", ProjectTrashResponseSchema);
projectLifecycle("restore", "restoreProject", ProjectRestoreResponseSchema);
projectLifecycle("purge", "purgeProject", ProjectPurgeResponseSchema);
app.post(
"/api/v1/support/check",
{