From 59a29a1e63de0c0026a2880947ead016937094ae Mon Sep 17 00:00:00 2001 From: nate Date: Thu, 9 Apr 2026 07:55:38 +0400 Subject: [PATCH] fix: improve auth --- apps/status/src/index.ts | 37 +++++++++-- apps/status/src/views/password.ejs | 4 +- apps/web/src/routes/dashboard.ts | 11 +++- apps/web/src/views/status-page-edit.ejs | 83 +++++++++++++++++++++++-- 4 files changed, 122 insertions(+), 13 deletions(-) diff --git a/apps/status/src/index.ts b/apps/status/src/index.ts index ba2821f..e482e51 100644 --- a/apps/status/src/index.ts +++ b/apps/status/src/index.ts @@ -103,7 +103,11 @@ async function renderHtml(slug: string, request: Request): Promise { const page = await loadStatusPage(slug); if (!page) return notFound(); if (!isAuthorised(page, request)) { - return new Response(eta.render("password", { title: page.title, slug: page.slug, error: null }), { + // Do NOT pass page.title to the password template — that would let any + // OSINT scraper iterating slugs harvest the human-readable name of every + // private page without ever authenticating. Slug is fine: it's already + // in the URL the visitor typed. + return new Response(eta.render("password", { slug: page.slug, error: null }), { status: 401, headers: { "content-type": "text/html; charset=utf-8", ...NO_STORE_HEADERS }, }); @@ -302,9 +306,9 @@ const app = new Elysia() const password = String(form.get("password") ?? ""); const ok = await checkPassword(password, page.password_hash); if (!ok) { - return new Response(eta.render("password", { title: page.title, slug: page.slug, error: "Wrong password" }), { + return new Response(eta.render("password", { slug: page.slug, error: "Wrong password" }), { status: 401, - headers: { "content-type": "text/html; charset=utf-8" }, + headers: { "content-type": "text/html; charset=utf-8", ...NO_STORE_HEADERS }, }); } return new Response(null, { @@ -316,7 +320,32 @@ const app = new Elysia() const port = Number(process.env.STATUS_PORT ?? 3003); const server = Bun.serve({ port, - fetch(req) { return app.handle(req); }, + // Wrap app.handle in a try/catch so any unexpected throw — Postgres + // connection blip, template render error, missing env var, etc. — turns + // into a generic 500 with an opaque body. Without this wrapper Bun's + // default error path may include framework details, file paths, or stack + // traces in the response, which would leak internal layout to anyone who + // could trigger an exception. We log the real reason server-side. + async fetch(req) { + try { + return await app.handle(req); + } catch (err) { + console.error("[fetch] unhandled error:", err); + return new Response("Internal server error", { + status: 500, + headers: { "content-type": "text/plain; charset=utf-8", ...NO_STORE_HEADERS }, + }); + } + }, + error(err) { + // Belt-and-suspenders: even if Bun catches an error before our fetch + // wrapper sees it, return a generic body rather than the default page. + console.error("[server] error:", err); + return new Response("Internal server error", { + status: 500, + headers: { "content-type": "text/plain; charset=utf-8", ...NO_STORE_HEADERS }, + }); + }, }); console.log(`PingQL status service running at http://localhost:${server.port}`); diff --git a/apps/status/src/views/password.ejs b/apps/status/src/views/password.ejs index fd6869a..b7e7d5f 100644 --- a/apps/status/src/views/password.ejs +++ b/apps/status/src/views/password.ejs @@ -4,7 +4,7 @@ - <%= it.title %> — Password required + Password required