import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { readFile } from "node:fs/promises"; import { renderAdmin, renderLogin } from "./admin"; import { type BoardCard, deleteRemoteImage, localUploadPath, publicCards, readCards, saveImage, storageMode, writeCards } from "./store"; const adminCookie = "pozor_admin"; const port = Number(process.env.PORT || 3001); const html = (body: string, init?: ResponseInit) => new Response(body, { ...init, headers: { "Content-Type": "text/html; charset=utf-8", ...(init?.headers || {}) } }); const redirect = (to: string, init?: ResponseInit) => new Response(null, { status: 303, ...init, headers: { Location: to, ...(init?.headers || {}) } }); const json = (body: unknown, init?: ResponseInit) => Response.json(body, { ...init, headers: { "Access-Control-Allow-Origin": "*", ...(init?.headers || {}) } }); function parseCookies(request: Request) { return Object.fromEntries( (request.headers.get("cookie") || "") .split(";") .map((part) => part.trim()) .filter(Boolean) .map((part) => { const index = part.indexOf("="); return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))]; }) ); } async function authToken() { const password = getAdminPassword(); if (!password) { return ""; } const data = new TextEncoder().encode(password); const digest = await crypto.subtle.digest("SHA-256", data); return Array.from(new Uint8Array(digest)) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } async function isAuthed(request: Request) { const token = await authToken(); return Boolean(token) && parseCookies(request)[adminCookie] === token; } function getAdminPassword() { return process.env.ADMIN_PASSWORD?.trim() || ""; } async function cardFromForm(form: FormData, existing?: BoardCard): Promise { const now = new Date().toISOString(); const title = String(form.get("title") || "").trim(); const comment = String(form.get("comment") || "").trim(); let imageUrl = String(form.get("imageUrl") || "").trim(); const file = form.get("image"); if (file instanceof File && file.size > 0) { if (existing?.imageUrl) { await deleteRemoteImage(existing.imageUrl); } imageUrl = await saveImage(file); } return { id: existing?.id || crypto.randomUUID(), title, comment, imageUrl, published: form.get("published") === "on", createdAt: existing?.createdAt || now, updatedAt: now }; } const app = new Elysia() .use( swagger({ path: "/docs", documentation: { info: { title: "Pozor Board API", version: "1.0.0", description: "API and admin endpoints for the dark-only shame board. Public routes expose published cards; admin routes require the admin session cookie." }, servers: [ { url: process.env.PUBLIC_API_URL || `http://localhost:${port}`, description: "API server" } ], tags: [ { name: "public", description: "Published board data" }, { name: "admin", description: "Admin panel and card management" }, { name: "uploads", description: "Local development uploads" } ] } }) ) .get("/openapi.json", async ({ request }) => { const url = new URL("/docs/json", request.url); return fetch(url); }, { detail: { tags: ["public"], summary: "OpenAPI JSON alias", description: "Redirects to the generated OpenAPI document." } }) .get("/", () => redirect("/admin"), { detail: { tags: ["admin"], summary: "Redirect to admin panel" } }) .get("/api/cards", async () => { const cards = publicCards(await readCards()); return json({ docs: cards }); }, { detail: { tags: ["public"], summary: "List published cards", description: "Returns cards visible on the public Astro board." } }) .get("/api/admin/cards", async ({ request }) => { if (!(await isAuthed(request))) { return json({ error: "Unauthorized" }, { status: 401 }); } return json({ docs: await readCards() }); }, { detail: { tags: ["admin"], summary: "List all cards", description: "Requires a valid admin session cookie." } }) .get("/uploads/:filename", async ({ params }) => { try { const file = await readFile(localUploadPath(params.filename)); return new Response(file); } catch { return new Response("Not found", { status: 404 }); } }, { detail: { tags: ["uploads"], summary: "Read local uploaded file", description: "Only used in local file storage mode. Blob uploads return public Blob URLs." } }) .get("/admin", async ({ request }) => { if (!(await isAuthed(request))) { return html(renderLogin(getAdminPassword() ? "" : "Set ADMIN_PASSWORD in .env to enable login.")); } return html(renderAdmin(await readCards(), { storageMode })); }, { detail: { tags: ["admin"], summary: "Admin panel" } }) .post("/admin/login", async ({ request }) => { const form = await request.formData(); const password = String(form.get("password") || ""); const adminPassword = getAdminPassword(); if (!adminPassword) { return html(renderLogin("ADMIN_PASSWORD is not set."), { status: 403 }); } if (adminPassword && password !== adminPassword) { return html(renderLogin("Wrong password."), { status: 401 }); } return redirect("/admin", { headers: { "Set-Cookie": `${adminCookie}=${await authToken()}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000` } }); }, { detail: { tags: ["admin"], summary: "Login to admin panel", description: "Accepts form data with a password field." } }) .post("/admin/logout", () => redirect("/admin", { headers: { "Set-Cookie": `${adminCookie}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0` } }), { detail: { tags: ["admin"], summary: "Logout from admin panel" } } ) .post("/admin/cards", async ({ request }) => { if (!(await isAuthed(request))) { return redirect("/admin"); } const form = await request.formData(); const cards = await readCards(); const id = String(form.get("id") || ""); const existing = cards.find((card) => card.id === id); const nextCard = await cardFromForm(form, existing); const nextCards = existing ? cards.map((card) => (card.id === existing.id ? nextCard : card)) : [nextCard, ...cards]; await writeCards(nextCards); return redirect("/admin"); }, { detail: { tags: ["admin"], summary: "Create or update a card", description: "Requires a valid admin session cookie. Accepts multipart form data." } }) .post("/admin/cards/delete", async ({ request }) => { if (!(await isAuthed(request))) { return redirect("/admin"); } const form = await request.formData(); const id = String(form.get("id") || ""); const cards = await readCards(); const existing = cards.find((card) => card.id === id); if (existing?.imageUrl) { await deleteRemoteImage(existing.imageUrl); } await writeCards(cards.filter((card) => card.id !== id)); return redirect("/admin"); }, { detail: { tags: ["admin"], summary: "Delete a card", description: "Requires a valid admin session cookie. Accepts form data with an id field." } }); export default app; if (process.env.VERCEL !== "1") { app.listen(port); console.log(`Pozor admin: http://localhost:${port}/admin`); }