avatars.ts
⎇
Raw
1import { Elysia, t } from "elysia";
2import { MAX_USER_UPLOAD_BYTES } from "../config.ts";
3import { db } from "../db/index.ts";
4import { requireAuth, resolveSession } from "../middleware/session.ts";
5import {
6 avatarJxlPath,
7 createDefaultAvatar,
8 processAndStoreAvatar,
9} from "../services/avatar.ts";
10
11const CACHE = "public, max-age=31536000, immutable";
12
13async function bumpAvatarVersion(userId: number): Promise<void> {
14 await db
15 .updateTable("users")
16 .set((eb) => ({ avatar_version: eb("avatar_version", "+", 1) }))
17 .where("id", "=", userId)
18 .execute();
19}
20
21export const avatarRoutes = new Elysia()
22 .guard({ cookie: t.Cookie({ session: t.Optional(t.String()) }) })
23
24 .get("/avatars/:id", async ({ params, request }) => {
25 const userId = parseInt(params.id, 10);
26 if (Number.isNaN(userId))
27 return new Response("Not found", { status: 404 });
28
29 if (!new URL(request.url).searchParams.has("v"))
30 return new Response("Not found", { status: 404 });
31
32 return new Response(Bun.file(avatarJxlPath(userId)), {
33 headers: {
34 "Content-Type": "image/jxl",
35 "Cache-Control": CACHE,
36 },
37 });
38 })
39
40 .post(
41 "/settings/avatar",
42 async ({ body, cookie }) => {
43 const user = await resolveSession(cookie.session.value);
44 const deny = requireAuth(user);
45 if (deny) return deny;
46 if (body.avatar.size > MAX_USER_UPLOAD_BYTES) {
47 return new Response("Avatar file too large", { status: 400 });
48 }
49 const buffer = Buffer.from(await body.avatar.arrayBuffer());
50 try {
51 await processAndStoreAvatar(user!.id, buffer);
52 } catch (err) {
53 if (
54 err instanceof Error &&
55 err.message === "Invalid image type"
56 ) {
57 return new Response("File is not a supported image type", {
58 status: 400,
59 });
60 }
61 throw err;
62 }
63 await bumpAvatarVersion(user!.id);
64 return new Response(null, {
65 status: 302,
66 headers: { Location: "/settings" },
67 });
68 },
69 {
70 body: t.Object({ avatar: t.File() }),
71 },
72 )
73
74 .post("/settings/avatar/delete", async ({ cookie }) => {
75 const user = await resolveSession(cookie.session.value);
76 const deny = requireAuth(user);
77 if (deny) return deny;
78 await createDefaultAvatar(user!.id, user!.username);
79 await bumpAvatarVersion(user!.id);
80 return new Response(null, {
81 status: 302,
82 headers: { Location: "/settings" },
83 });
84 });
85