import { Router } from "@oak/oak"; import { authMiddleware } from "../middleware/auth.ts"; import { getUserById, updateUserAvatar } from "../services/user-service.ts"; import { updateClientAvatar } from "../services/ws-service.ts"; import { APIErrorCode, APIException } from "../model/interfaces.ts"; import { AVATARS_DIR, detectImageMime, MAX_IMAGE_SIZE, serveUploadedFile, } from "../utils/upload.ts"; const router = new Router(); router.post("/api/avatars/me", authMiddleware, async (ctx) => { const authPayload = ctx.state.user; const contentType = ctx.request.headers.get("content-type") ?? ""; if (!contentType.includes("multipart/form-data")) { throw new APIException( APIErrorCode.BAD_REQUEST, 400, "Expected multipart/form-data", ); } const body = await ctx.request.body.formData(); const file = body.get("file"); if (!(file instanceof File)) { throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Missing file field"); } if (file.size > MAX_IMAGE_SIZE) { throw new APIException( APIErrorCode.BAD_REQUEST, 400, "File too large (max 5 MB)", ); } const data = new Uint8Array(await file.arrayBuffer()); const mime = detectImageMime(data); if (!mime) { throw new APIException( APIErrorCode.BAD_REQUEST, 400, "File content is not a recognised image (JPEG, PNG, GIF, WebP)", ); } await Deno.mkdir(AVATARS_DIR, { recursive: true }); await Deno.writeFile(`${AVATARS_DIR}/${authPayload.userId}`, data); updateUserAvatar(authPayload.userId, mime); updateClientAvatar(authPayload.userId, mime); const user = getUserById(authPayload.userId); ctx.response.status = 200; ctx.response.body = { success: true, data: user }; }); router.get("/api/avatars/:userId", async (ctx) => { const { userId } = ctx.params; let user; try { user = getUserById(userId); } catch { ctx.response.status = 404; return; } if (!user.avatarMime) { ctx.response.status = 404; return; } await serveUploadedFile(ctx, `${AVATARS_DIR}/${userId}`, user.avatarMime); }); export default router;