Files
exams/backend/src/routes/account/cdn.ts

181 lines
5.6 KiB
TypeScript

import { COOKIENAME, fileRouter } from "../../";
import { authCheck } from "../../lib/Auth";
import { minioClient, CDN_BUCKET, getPublicUrl } from "../../lib/CDN";
import crypto from "crypto";
export = new fileRouter.Path("/").http(
"POST",
"/api/account/cdn/upload",
(http) =>
http.onRequest(async (ctr) => {
const cookie = ctr.cookies.get(COOKIENAME) || null;
const apiHeader = (ctr.headers && ctr.headers.get)
? (ctr.headers.get("api-authentication") as string) || null
: null;
const auth = await authCheck(cookie, apiHeader);
if (!auth.state) {
return ctr
.status(ctr.$status.UNAUTHORIZED)
.print({ error: auth.message });
}
const [data, error] = await ctr.bindBody((z) =>
z.object({
images: z.array(z.object({
image: z.string().min(1), // base64
filename: z.string().min(1).max(200),
contentType: z.string().min(1).max(100),
})).min(1),
})
);
if (!data)
return ctr.status(ctr.$status.BAD_REQUEST).print({ error: error.toString() });
try {
const urls: string[] = [];
const ALLOWED_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "webp", "pdf"]);
for (const item of data.images) {
const buffer = Buffer.from(item.image, 'base64');
const rawExtension = item.filename.split('.').pop() || "";
const extension = rawExtension.toLowerCase();
if (!ALLOWED_EXTENSIONS.has(extension)) {
return ctr
.status(ctr.$status.BAD_REQUEST)
.print({ error: "Unsupported file extension" });
}
const objectName = `${auth.userId}/${crypto.randomUUID()}.${extension}`;
await minioClient.putObject(CDN_BUCKET, objectName, buffer, buffer.length, {
'Content-Type': item.contentType,
});
urls.push(getPublicUrl(objectName));
}
return ctr.print({
success: true,
urls: urls
});
} catch (error: unknown) {
console.error(error);
const details =
error instanceof Error ? error.message : "Unknown error during image upload";
return ctr.status(ctr.$status.INTERNAL_SERVER_ERROR).print({
error: "Failed to upload images to CDN",
errorCode: "CDN_UPLOAD_FAILED",
details
});
}
})
).http(
"GET",
"/api/account/cdn/list",
(http) =>
http.onRequest(async (ctr) => {
const cookie = ctr.cookies.get(COOKIENAME) || null;
const apiHeader = (ctr.headers && ctr.headers.get)
? (ctr.headers.get("api-authentication") as string) || null
: null;
const auth = await authCheck(cookie, apiHeader);
if (!auth.state) {
return ctr
.status(ctr.$status.UNAUTHORIZED)
.print({ error: auth.message });
}
try {
const objects: any[] = [];
const stream = minioClient.listObjectsV2(CDN_BUCKET, `${auth.userId}/`, true);
for await (const obj of stream) {
objects.push({
name: obj.name,
url: getPublicUrl(obj.name),
size: obj.size,
lastModified: obj.lastModified
});
}
return ctr.print({
success: true,
images: objects
});
} catch (e) {
console.error(e);
return ctr.status(ctr.$status.INTERNAL_SERVER_ERROR).print({
error: "Failed to list images from CDN"
});
}
})
).http(
"DELETE",
"/api/account/cdn/delete",
(http) =>
http.onRequest(async (ctr) => {
const cookie = ctr.cookies.get(COOKIENAME) || null;
const apiHeader = (ctr.headers && ctr.headers.get)
? (ctr.headers.get("api-authentication") as string) || null
: null;
const auth = await authCheck(cookie, apiHeader);
if (!auth.state) {
return ctr
.status(ctr.$status.UNAUTHORIZED)
.print({ error: auth.message });
}
const [data, error] = await ctr.bindBody((z) =>
z.object({
urls: z.array(z.string().url()).min(1),
})
);
if (!data)
return ctr.status(ctr.$status.BAD_REQUEST).print({ error: error.toString() });
try {
const objectsToDelete: string[] = [];
for (const url of data.urls) {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split('/').filter(Boolean);
// Expected: [bucket, userId, filename]
if (pathParts.length < 3) continue;
const bucket = pathParts[0];
const userId = pathParts[1];
const filename = pathParts.slice(2).join('/');
const objectName = `${userId}/${filename}`;
if (bucket === CDN_BUCKET && userId === auth.userId) {
objectsToDelete.push(objectName);
}
}
if (objectsToDelete.length === 0) {
return ctr.status(ctr.$status.BAD_REQUEST).print({ error: "No valid objects to delete" });
}
await minioClient.removeObjects(CDN_BUCKET, objectsToDelete);
return ctr.print({
success: true,
message: `${objectsToDelete.length} image(s) deleted successfully`
});
} catch (e) {
console.error(e);
return ctr.status(ctr.$status.INTERNAL_SERVER_ERROR).print({
error: "Failed to delete images"
});
}
})
);