Commitccec83a
fix(standups): stop crashing with a raw 500 for anonymous visitors
fix(standups): stop crashing with a raw 500 for anonymous visitors
All three standup routes did `c.get("user")!` and dereferenced it
immediately, assuming a logged-in caller. The auth gate meant to
prevent that used a wildcard pattern (`standups.use("/standups*",
requireAuth)`) that didn't cover /api/standups/preview at all (it's
not under /standups) and, per live reproduction, didn't reliably gate
bare /standups either -- anonymous GET /standups threw "TypeError:
null is not an object (evaluating 'user.id')" and returned a raw 500.
Found by a full production HTTP sweep across all 201 static page
routes (2 real 500s out of 201 -- this is the other one, see the
gluecron-write commit for the standups fix). There was no test
coverage of these routes through app.request() before this, which is
exactly how a wildcard-matching bug like this survives.
Fixed by putting requireAuth on each route explicitly instead of
relying on wildcard-pattern matching.2 files changed+33−4ccec83ab0455c50e7a1ca78d4975f41a5e426931
2 changed files+33−4
Modifiedsrc/__tests__/ai-standup.test.ts+30−0View fileUnifiedSplit
@@ -18,6 +18,36 @@ import {
1818
1919const HAS_DB = Boolean(process.env.DATABASE_URL);
2020
21describe("standups routes — anonymous access", () => {
22 // Regression test: all three routes did `c.get("user")!` and crashed with
23 // a raw 500 (TypeError: null is not an object) for anonymous callers.
24 // requireAuth was wired via a wildcard pattern ("/standups*") that didn't
25 // actually cover /api/standups/preview at all, and live evidence showed
26 // it didn't reliably cover bare /standups either. Found via a full
27 // production HTTP sweep, not a doc or a prior test — there was no
28 // coverage of these routes through app.request() before this.
29 it("GET /standups redirects to login instead of crashing", async () => {
30 const app = (await import("../app")).default;
31 const res = await app.request("/standups");
32 expect(res.status).toBe(302);
33 expect(res.headers.get("location")).toContain("/login");
34 });
35
36 it("POST /standups/preview redirects to login instead of crashing", async () => {
37 const app = (await import("../app")).default;
38 const res = await app.request("/standups/preview", { method: "POST" });
39 expect(res.status).toBe(302);
40 expect(res.headers.get("location")).toContain("/login");
41 });
42
43 it("GET /api/standups/preview redirects to login instead of crashing", async () => {
44 const app = (await import("../app")).default;
45 const res = await app.request("/api/standups/preview");
46 expect(res.status).toBe(302);
47 expect(res.headers.get("location")).toContain("/login");
48 });
49});
50
2151// ---------------------------------------------------------------------------
2252// Pure helpers — no DB, no AI client.
2353// ---------------------------------------------------------------------------
Modifiedsrc/routes/standups.tsx+3−4View fileUnifiedSplit
@@ -27,7 +27,6 @@ import {
2727const standups = new Hono<AuthEnv>();
2828
2929standups.use("*", softAuth);
30standups.use("/standups*", requireAuth);
3130
3231function formatStamp(d: Date): string {
3332 try {
@@ -41,7 +40,7 @@ function scopeLabel(scope: string): string {
4140 return scope === "weekly" ? "Weekly" : "Daily";
4241}
4342
44standups.get("/standups", async (c) => {
43standups.get("/standups", requireAuth, async (c) => {
4544 const user = c.get("user")!;
4645 const [recent, prefs] = await Promise.all([
4746 listRecentStandups(user.id, 30),
@@ -181,7 +180,7 @@ standups.get("/standups", async (c) => {
181180 );
182181});
183182
184standups.post("/standups/preview", async (c) => {
183standups.post("/standups/preview", requireAuth, async (c) => {
185184 const user = c.get("user")!;
186185 // Bypass the dedupe check so the on-demand button always produces a row.
187186 await deliverStandup({
@@ -193,7 +192,7 @@ standups.post("/standups/preview", async (c) => {
193192});
194193
195194// Optional JSON endpoint — handy for /admin debugging and tests.
196standups.get("/api/standups/preview", async (c) => {
195standups.get("/api/standups/preview", requireAuth, async (c) => {
197196 const user = c.get("user")!;
198197 const scope = c.req.query("scope") === "weekly" ? "weekly" : "daily";
199198 const result = await generateStandup({ userId: user.id, scope });
200199