import cors from "@elysiajs/cors"; import { AttachmentId, PieceId, RequestId, SessionId, Sha256 } from "common"; import * as Function from "common/Function"; import { Elysia, error, form, t } from "elysia"; import { sql } from "kysely"; import { generateSessionId, initDatabase } from "./database"; const tbranded = () => t.Transform(t.String()) .Decode(Function.unsafeCoerce) .Encode(Function.unsafeCoerce); const app = new Elysia() .use(cors({ origin: "localhost:5173" })) .decorate("db", await initDatabase()) .resolve(async ({ db, cookie }) => { await db .deleteFrom("Session") .where(sql`datetime()`, ">=", "expiresAt") .execute(); const sessionId = cookie.sessionId.value; if (sessionId === undefined) { return { user: null }; } const res = await db .updateTable("Session") .set({ expiresAt: sql`datetime('now', '+7 days') `}) .where("sessionId", "=", SessionId(sessionId)) .returning(["userId"]) .executeTakeFirst(); if (res === undefined) { return { user: null }; } const user = await db .selectFrom("User") .selectAll() .where("userId", "=", res.userId) .executeTakeFirst(); if (user === undefined) { return { user: null }; } return { user }; }) .onTransform(async ({ db, request, server }) => { const requestId = RequestId(Bun.randomUUIDv7("hex")); const timestamp = new Date().toISOString(); const { method } = request; const url = new URL(request.url); const { pathname } = url; const query = JSON.stringify(Object.fromEntries(url.searchParams.entries())); const ip = server?.requestIP(request)?.address ?? null; await db .insertInto("AccessLog") .values({ requestId, timestamp, method, pathname, query, ip }) .execute(); console.log(`${timestamp} ${method} ${request.url} ${ip}`); }) // --- MARK: AUTHENTICATION ------------------------------------------------ .get("/me", async ({ user }) => { if (user === null) { return error("Unauthorized"); } return { userId: user.userId, username: user.username, admin: user.admin !== 0, }; }) .post("/login", async ({ db, body: { username, password }, cookie }) => { const user = await db .selectFrom("User") .selectAll() .where("username", "=", username) .executeTakeFirst(); if (user === undefined) { return error("Unauthorized", "Invalid username or password"); } const valid = await Bun.password.verify(password, user.password); if (!valid) { return error("Unauthorized", "Invalid username or password"); } const sessionId = generateSessionId(); await db .insertInto("Session") .values({ sessionId, userId: user.userId, expiresAt: sql`datetime('now', '+7 days')` }) .execute(); const expiresAt = new Date().getTime() + 604800000; cookie.sessionId.set({ value: sessionId, expires: new Date(expiresAt), httpOnly: true, sameSite: "none", secure: true, }); return { userId: user.userId, username: user.username, admin: user.admin !== 0, }; }, { body: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 1 }), }), }) .post("/logout", async ({ db, cookie, set }) => { set.status = "No Content"; const sessionCookie = cookie.sessionId; sessionCookie.remove(); const sessionId = sessionCookie.value; if (sessionId === undefined) { return; } await db .deleteFrom("Session") .where("sessionId", "=", SessionId(sessionId)) .execute(); }) // --- MARK: PIECE CRUD ---------------------------------------------------- .post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, user }) => { if (user === null) { return error("Unauthorized"); } const pieceId = PieceId(Bun.randomUUIDv7()); const res = await db .insertInto("Piece") .values({ pieceId, name, composer, lyricist, arranger, createdBy: user.userId, createdAt: sql`datetime()` }) .returningAll() .executeTakeFirstOrThrow(); return res; }, { body: t.Object({ name: t.String({ minLength: 1 }), composer: t.Nullable(t.String({ minLength: 1 })), lyricist: t.Nullable(t.String({ minLength: 1 })), arranger: t.Nullable(t.String({ minLength: 1 })), }), }) .get("/piece", async ({ db, query, user }) => { if (user === null) { return error("Unauthorized"); } let q = db .selectFrom("Piece") .selectAll() .orderBy(["name", "composer", "arranger"]) .limit(100); if (query.id !== undefined) { q = q.where("pieceId", "=", query.id); } const res = await q.execute(); return res; }, { query: t.Object({ id: t.Optional(tbranded()), }), }) .put("/piece/:pieceId", async ({ db, body: { name, composer, lyricist, arranger }, params: { pieceId }, user }) => { if (user === null) { return error("Unauthorized"); } const res = await db .updateTable("Piece") .set({ name, composer, lyricist, arranger, modifiedBy: user.userId, modifiedAt: sql`datetime()` }) .where("pieceId", "=", pieceId) .returningAll() .execute(); if (res.length === 0) { return error("Not Found"); } return res[0]; }, { body: t.Object({ name: t.String({ minLength: 1 }), composer: t.Nullable(t.String({ minLength: 1 })), lyricist: t.Nullable(t.String({ minLength: 1 })), arranger: t.Nullable(t.String({ minLength: 1 })), }), params: t.Object({ pieceId: tbranded(), }) }) .delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, user }) => { if (user === null) { return error("Unauthorized"); } const res = await db .deleteFrom("Piece") .where("pieceId", "=", pieceId) .returningAll() .execute(); if (res.length === 0) { return error("Not Found"); } set.status = "No Content"; }, { params: t.Object({ pieceId: tbranded(), }), }) // --- MARK: ATTACHMENT CRUD ----------------------------------------------- .post("piece/:pieceId/attachment", async ({ db, body: { filename, mediaType, data }, params: { pieceId }, user }) => { if (user === null) { return error("Unauthorized"); } const attachmentId = AttachmentId(Bun.randomUUIDv7()); const dataArray = new Uint8Array(await data.arrayBuffer()); const sha256 = Sha256(new Uint8Array(Bun.SHA256.byteLength)); Bun.SHA256.hash(dataArray, sha256); await db .insertInto("File") .values({ sha256, data: dataArray }) .onConflict((cb) => cb.column("sha256").doNothing()) .execute(); const res = await db .insertInto("Attachment") .values({ attachmentId, pieceId, sha256, filename, mediaType, createdBy: user.userId, createdAt: sql`datetime()` }) .returningAll() .executeTakeFirstOrThrow(); return res; }, { body: t.Object({ filename: t.String({ minLength: 1 }), mediaType: t.String({ minLength: 1 }), data: t.File(), }), params: t.Object({ pieceId: tbranded(), }), }) .get("piece/:pieceId/attachment", async ({ db, params: { pieceId }, user }) => { if (user === null) { return error("Unauthorized"); } const res = await db .selectFrom("Attachment") .selectAll() .where("pieceId", "=", pieceId) .orderBy("filename") .execute(); return res; }, { params: t.Object({ pieceId: tbranded(), }), }) /* NOTE The piece ID is reduntant, because attachment IDs are unique for the * entire DB, not just per piece. However, we consider a piece to be the * sole owner of an attachment, i.e. attachments are not shared (attachments * are deduplicated on file storage level by their SHA-256 hash). Thus, we * reflect the ownership in the URLs. */ .get("piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, user }) => { if (user === null) { return error("Unauthorized"); } const res = await db .selectFrom("File") .innerJoin("Attachment", "File.sha256", "Attachment.sha256") .select(["Attachment.filename", "Attachment.mediaType", "File.data"]) .where((eb) => eb.and([ eb("Attachment.pieceId", "=", pieceId), eb("Attachment.attachmentId", "=", attachmentId), ])) .executeTakeFirst(); if (res === undefined) { return error("Not Found"); } return form({ filename: res.filename, mediaType: res.mediaType, data: new File([res.data], res.filename, { type: res.mediaType }), }); }, { params: t.Object({ pieceId: tbranded(), attachmentId: tbranded(), }), }) .put("piece/:pieceId/attachment/:attachmentId", async ({ db, body: { filename }, params: { pieceId, attachmentId }, user }) => { if (user === null) { return error("Unauthorized"); } const res = await db .updateTable("Attachment") .set({ filename, modifiedBy: user.userId, modifiedAt: sql`datetime()` }) .where((eb) => eb.and([ eb("pieceId", "=", pieceId), eb("attachmentId", "=", attachmentId), ])) .returningAll() .execute(); if (res.length === 0) { return error("Not Found"); } return res[0]; }, { body: t.Object({ filename: t.String({ minLength: 1 }), }), params: t.Object({ pieceId: tbranded(), attachmentId: tbranded(), }), }) .delete("piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, set, user }) => { if (user === null) { return error("Unauthorized"); } const res = await db .deleteFrom("Attachment") .where((eb) => eb.and([ eb("pieceId", "=", pieceId), eb("attachmentId", "=", attachmentId), ])) .returningAll() .execute(); if (res.length === 0) { return error("Not Found"); } set.status = "No Content"; }, { params: t.Object({ pieceId: tbranded(), attachmentId: tbranded(), }), }) // ------------------------------------------------------------------------- .listen(process.env.PORT || 3000); export type App = typeof app;