harden database interactions and improve error handling

This commit is contained in:
ImBenji
2026-04-28 14:36:13 +01:00
parent 5a4caaf1d0
commit e1f168a302
29 changed files with 869 additions and 241 deletions
+4 -1
View File
@@ -15,7 +15,10 @@ export default defineEventHandler(async (event) => {
if (!body.title.trim()) throw createError({ statusCode: 400, message: "Title cannot be empty" });
updates.title = body.title.trim();
}
if (body.subject !== undefined) updates.subject = body.subject;
if (body.subject !== undefined) {
if (!body.subject.trim()) throw createError({ statusCode: 400, message: "Subject cannot be empty" });
updates.subject = body.subject.trim();
}
if (Object.keys(updates).length === 0) {
throw createError({ statusCode: 400, message: "Nothing to update" });
+35
View File
@@ -0,0 +1,35 @@
import { db } from "../../../db/index";
import { courses, topics, lessons } from "../../../db/schema";
import { eq, inArray } from "drizzle-orm";
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")!;
const course = await db.query.courses.findFirst({ where: eq(courses.id, id) });
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
const topicRows = await db.query.topics.findMany({
where: eq(topics.courseId, id),
orderBy: (t, { asc }) => asc(t.order),
});
let lessonTopicIds: Set<string> = new Set();
if (topicRows.length > 0) {
const topicIds = topicRows.map((t) => t.id);
const lessonRows = await db.query.lessons.findMany({
where: inArray(lessons.topicId, topicIds),
});
lessonTopicIds = new Set(lessonRows.map((l) => l.topicId));
}
return {
status: course.status,
stage: course.stage,
topics: topicRows.map((t) => ({
id: t.id,
status: t.status,
hasLesson: lessonTopicIds.has(t.id),
})),
};
});
+33 -4
View File
@@ -7,9 +7,15 @@ import { resolve } from "path";
import { parsePdf } from "../../../utils/parsePdf";
import { detectUploadType } from "../../../utils/detectUploadType";
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")!;
if (!UUID_RE.test(id)) {
throw createError({ statusCode: 400, message: "Invalid course id" });
}
const course = await db.query.courses.findFirst({ where: eq(courses.id, id) });
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
@@ -20,21 +26,40 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, message: "file is required" });
}
// 50mb limit
if (file.size > 50 * 1024 * 1024) {
throw createError({ statusCode: 400, message: "File exceeds 50MB limit" });
}
const buffer = Buffer.from(await file.arrayBuffer());
// check pdf magic bytes: %PDF = 0x25 0x50 0x44 0x46
if (buffer[0] !== 0x25 || buffer[1] !== 0x50 || buffer[2] !== 0x44 || buffer[3] !== 0x46) {
throw createError({ statusCode: 400, message: "File does not appear to be a valid PDF" });
}
const uploadDir = resolve(process.cwd(), "uploads", id);
await mkdir(uploadDir, { recursive: true });
const uploadId = randomUUID();
const safeFilename = `${uploadId}-${file.name.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
const storedPath = resolve(uploadDir, safeFilename);
const buffer = Buffer.from(await file.arrayBuffer());
// safeFilename shouldnt be just dots/underscores
if (/^[._]+$/.test(safeFilename.replace(uploadId + "-", ""))) {
throw createError({ statusCode: 400, message: "Invalid filename" });
}
const storedPath = resolve(uploadDir, safeFilename);
await writeFile(storedPath, buffer);
let extractedText: string | null = null;
let pdfWarning: string | undefined;
try {
extractedText = await parsePdf(buffer);
} catch {
// non-fatal
} catch (err: any) {
console.error(`[upload] PDF text extraction failed for file size ${buffer.length}: ${err?.message ?? err}`);
pdfWarning = "PDF text extraction failed";
}
const detectedType = await detectUploadType(file.name, extractedText ?? "");
@@ -48,5 +73,9 @@ export default defineEventHandler(async (event) => {
extractedText,
});
if (pdfWarning) {
return { uploadId, filename: file.name, type: detectedType, warning: pdfWarning };
}
return { uploadId, filename: file.name, type: detectedType };
});