Add approved asset removal controls
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { watch } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import cors from "@fastify/cors";
|
||||
import multipart from "@fastify/multipart";
|
||||
@@ -133,6 +133,20 @@ const storeUploadedFile = async (filePart: ParsedMultipartFile, assetId: string)
|
||||
return `/uploads/${relativePath}`;
|
||||
};
|
||||
|
||||
const resolveUploadKeyPath = (uploadKey: string | undefined) => {
|
||||
if (!uploadKey || !uploadKey.startsWith("/uploads/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = uploadKey.slice("/uploads/".length);
|
||||
const absolutePath = path.resolve(config.storageDir, relativePath);
|
||||
if (!absolutePath.startsWith(path.resolve(config.storageDir))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return absolutePath;
|
||||
};
|
||||
|
||||
const createSubmissionFromMultipart = async (
|
||||
store: StateStore,
|
||||
request: FastifyRequest,
|
||||
@@ -371,6 +385,29 @@ export const buildServer = async () => {
|
||||
return reply.status(204).send();
|
||||
});
|
||||
|
||||
app.delete<{ Params: { assetId: string } }>("/api/assets/:assetId", async (request, reply) => {
|
||||
const currentState = await store.read();
|
||||
const asset = currentState.photoAssets.find((entry) => entry.id === request.params.assetId);
|
||||
if (!asset) {
|
||||
return reply.status(404).send({ message: "Asset not found." });
|
||||
}
|
||||
|
||||
const submission = currentState.submissions.find((entry) => entry.id === asset.submissionId);
|
||||
if (submission?.source === "library_import") {
|
||||
return reply.status(400).send({ message: "Library assets should be removed from the approved bank without deleting the source file." });
|
||||
}
|
||||
|
||||
await store.deleteAsset(request.params.assetId);
|
||||
|
||||
const uploadPaths = [asset.originalKey, asset.thumbKey, asset.previewKey, asset.renderKey]
|
||||
.map((key) => resolveUploadKeyPath(key))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
|
||||
await Promise.all(uploadPaths.map((filePath) => rm(filePath, { force: true })));
|
||||
|
||||
return reply.status(204).send();
|
||||
});
|
||||
|
||||
app.post<{ Params: { cueId: string } }>("/api/cues/:cueId/fire", async (request, reply) => {
|
||||
await store.logCueEvent("cue_fired", { cueId: request.params.cueId });
|
||||
return reply.status(204).send();
|
||||
|
||||
@@ -580,6 +580,8 @@ export class StateStore {
|
||||
submission.status =
|
||||
payload.decision === "approved"
|
||||
? "approved_all"
|
||||
: payload.decision === "archive_only"
|
||||
? "archived"
|
||||
: payload.decision === "rejected"
|
||||
? "rejected"
|
||||
: "pending_moderation";
|
||||
@@ -711,4 +713,50 @@ export class StateStore {
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAsset(assetId: string) {
|
||||
return this.update((state) => {
|
||||
const asset = state.photoAssets.find((entry) => entry.id === assetId);
|
||||
if (!asset) {
|
||||
throw new Error("Asset not found.");
|
||||
}
|
||||
|
||||
const submission = state.submissions.find((entry) => entry.id === asset.submissionId);
|
||||
if (submission?.source === "library_import") {
|
||||
throw new Error("Library assets must be removed from the approved bank without deleting the source file.");
|
||||
}
|
||||
|
||||
state.photoAssets = state.photoAssets.filter((entry) => entry.id !== assetId);
|
||||
state.collections = state.collections.map((collection) => ({
|
||||
...collection,
|
||||
assetIds: collection.assetIds.filter((entry) => entry !== assetId),
|
||||
coverAssetId: collection.coverAssetId === assetId ? undefined : collection.coverAssetId
|
||||
}));
|
||||
state.cues = normalizeCueOrder(
|
||||
state.cues.map((cue) => ({
|
||||
...cue,
|
||||
assetIds: cue.assetIds?.filter((entry) => entry !== assetId)
|
||||
}))
|
||||
);
|
||||
state.moderationDecisions = state.moderationDecisions.filter((decision) => decision.assetId !== assetId);
|
||||
|
||||
const remainingSubmissionAssets = state.photoAssets.filter((entry) => entry.submissionId === asset.submissionId);
|
||||
if (remainingSubmissionAssets.length === 0 && submission) {
|
||||
state.submissions = state.submissions.filter((entry) => entry.id !== submission.id);
|
||||
state.consents = state.consents.filter((entry) => entry.id !== submission.consentId);
|
||||
state.sessionEvents = state.sessionEvents.filter((event) => {
|
||||
const eventAssetId = typeof event.payload.assetId === "string" ? event.payload.assetId : null;
|
||||
const eventSubmissionId = typeof event.payload.submissionId === "string" ? event.payload.submissionId : null;
|
||||
return eventAssetId !== assetId && eventSubmissionId !== submission.id;
|
||||
});
|
||||
} else {
|
||||
state.sessionEvents = state.sessionEvents.filter((event) => {
|
||||
const eventAssetId = typeof event.payload.assetId === "string" ? event.payload.assetId : null;
|
||||
return eventAssetId !== assetId;
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user