import { Hono } from "jsr:@hono/hono@4"; import { getAuthenticatedUser, getCookieDomain } from "../../core/session.ts"; import { deleteCookie, setCookie } from "jsr:@hono/hono@4/cookie"; import { encodeHex } from "jsr:@std/encoding@1/hex"; import { getClientIp, rateLimitWrapper } from "../../core/middleware.ts"; import { valkey } from "../../core/valkey.ts"; import { auditWrapper } from "../../core/audit.ts"; import { renderErrorToastFragment } from "../../core/error_fragments.tsx"; import * as Queries from "./queries.ts"; import { EventJoinPageFragment, EventSplashPageFragment, } from "./fragments.tsx"; import { eventsActionsRoutes } from "./events_actions_routes.ts"; import { eventStreamRoutes } from "./stream_routes.ts"; export const eventsRoutes = new Hono(); // Mount Actions and Stream sub-routers eventsRoutes.route("/", eventsActionsRoutes); eventsRoutes.route("/", eventStreamRoutes); // --------------------------------------------------------- // Join Event via Code or PIN // --------------------------------------------------------- eventsRoutes.post("/api/join", async (c) => { let code = ""; const contentType = c.req.header("content-type") || ""; const accept = c.req.header("accept") || ""; const wantsJson = accept.includes("application/json") && !accept.includes("text/html"); if (contentType.includes("application/x-www-form-urlencoded")) { const fd = await c.req.formData(); code = (fd.get("code") as string) || ""; } else { const body = await c.req.json().catch(() => ({})); code = body.code || ""; } if (!code || typeof code !== "string") { if (wantsJson) { return c.json({ error: "Event code or PIN is required" }, 400); } return c.html( renderErrorToastFragment("Event code or PIN is required"), 400, ); } const clientIp = getClientIp(c); const rateLimitKey = `ratelimit:join:fail:${clientIp}`; if (await rateLimitWrapper.isRateLimited(rateLimitKey, 5, 60000)) { if (wantsJson) { return c.json({ error: "Too Many Requests" }, 429); } return c.html(renderErrorToastFragment("Too Many Requests"), 429); } try { const event = await Queries.getEventBySlugOrPin(code); if (!event) { await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000); if (wantsJson) { return c.json( { error: "Invalid event code or workshop capacity reached" }, 404, ); } return c.html( renderErrorToastFragment( "Invalid event code or workshop capacity reached", ), 404, ); } // NAT-Safe Idempotent Re-entry: check if device already holds active session for this event const existingUser = await getAuthenticatedUser(c); if (existingUser && existingUser.username?.startsWith(`guest_`)) { const ttl = (Number(event.lifespan_hours) || 3) * 3600; await valkey.expire(existingUser.sessionId, ttl); deleteCookie(c, "session_id", { path: "/" }); const rpID = Deno.env.get("RP_ID"); const cookieDomain = getCookieDomain(rpID); setCookie(c, "session_id", existingUser.sessionId, { domain: cookieDomain, path: "/", httpOnly: true, secure: true, sameSite: "Lax", maxAge: ttl, }); let appDomain = ""; if (event.app_id) { const app = await Queries.getAppById(String(event.app_id)); if (app) { appDomain = app.domain || ""; } } const redirectUrl = appDomain ? `https://${appDomain}` : "/dashboard"; if (wantsJson) { return c.json({ success: true, sessionId: existingUser.sessionId, username: existingUser.username, redirectUrl, reused: true, }); } c.header("HX-Redirect", redirectUrl); return c.html(""); } const updatedEvent = await Queries.incrementEventSeats(String(event.id)); if (!updatedEvent) { await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000); if (wantsJson) { return c.json({ error: "Workshop capacity reached" }, 404); } return c.html(renderErrorToastFragment("Workshop capacity reached"), 404); } const guestUuid = crypto.randomUUID(); const eventShortId = String(updatedEvent.id).split("-")[0]; const username = `guest_${eventShortId}_${updatedEvent.seats_claimed}`; await Queries.createGuestUser( guestUuid, username, updatedEvent.name + " Attendee", String(updatedEvent.id), ); const randomBytes = crypto.getRandomValues(new Uint8Array(32)); const sessionId = `ay_sess_${encodeHex(randomBytes)}`; const label = `${updatedEvent.name} Seat #${updatedEvent.seats_claimed}`; const ttl = (Number(updatedEvent.lifespan_hours) || 3) * 3600; let customScopes = ["guest", "trial"]; let appDomain = ""; if (updatedEvent.app_id) { const app = await Queries.getAppById(String(updatedEvent.app_id)); if (app) { customScopes = [`app:${app.name}`, updatedEvent.role || "viewer"]; appDomain = app.domain || ""; } } const expiresAt = new Date(Date.now() + ttl * 1000); await Queries.createEventSession( sessionId, guestUuid, label, customScopes, expiresAt, ); await valkey.setex( sessionId, ttl, JSON.stringify({ uuid: guestUuid, username, account_status: "guest", customScopes, }), ); auditWrapper.auditLog(guestUuid, "event_seat_claimed", updatedEvent.id, { slug: updatedEvent.slug, name: updatedEvent.name, seatNumber: updatedEvent.seats_claimed, method: "web", }, getClientIp(c)); deleteCookie(c, "session_id", { path: "/" }); const rpID = Deno.env.get("RP_ID"); const cookieDomain = getCookieDomain(rpID); setCookie(c, "session_id", sessionId, { domain: cookieDomain, path: "/", httpOnly: true, secure: true, sameSite: "Lax", maxAge: ttl, }); const redirectUrl = appDomain ? `https://${appDomain}` : "/dashboard"; if (wantsJson) { return c.json({ success: true, sessionId, username, redirectUrl, }); } c.header("HX-Redirect", redirectUrl); return c.html(""); } catch (e: any) { console.error("[Events] Failed to join event:", e); if (wantsJson) { return c.json({ error: "Failed to join event" }, 500); } return c.html(renderErrorToastFragment("Failed to join event"), 500); } }); // --------------------------------------------------------- // CLI 1-Liner / Environment Ingress (/join/:slug) // --------------------------------------------------------- eventsRoutes.get("/join/:slug", async (c) => { const slug = c.req.param("slug") || ""; const format = c.req.query("format") || "html"; try { const event = await Queries.getEventBySlugOrPin(slug); if (!event) { if (format === "env" || format === "json") { return c.text("Invalid slug or workshop capacity reached", 404); } return c.redirect("/join?error=not_found", 302); } const updatedEvent = await Queries.incrementEventSeats(String(event.id)); if (!updatedEvent) { if (format === "env" || format === "json") { return c.text("Workshop capacity reached", 404); } return c.redirect("/join?error=capacity_reached", 302); } const guestUuid = crypto.randomUUID(); const eventShortId = String(updatedEvent.id).split("-")[0]; const username = `guest_${eventShortId}_${updatedEvent.seats_claimed}`; await Queries.createGuestUser( guestUuid, username, updatedEvent.name + " Attendee", String(updatedEvent.id), ); const randomBytes = crypto.getRandomValues(new Uint8Array(32)); const sessionId = `ay_sess_${encodeHex(randomBytes)}`; const label = `${updatedEvent.name} Seat #${updatedEvent.seats_claimed}`; const ttl = (Number(updatedEvent.lifespan_hours) || 3) * 3600; let customScopes = ["guest", "trial"]; if (updatedEvent.app_id) { const app = await Queries.getAppById(String(updatedEvent.app_id)); if (app) { customScopes = [`app:${app.name}`, updatedEvent.role || "viewer"]; } } const expiresAt = new Date(Date.now() + ttl * 1000); await Queries.createEventSession( sessionId, guestUuid, label, customScopes, expiresAt, ); await valkey.setex( sessionId, ttl, JSON.stringify({ uuid: guestUuid, username, account_status: "guest", customScopes, }), ); auditWrapper.auditLog(guestUuid, "event_seat_claimed", updatedEvent.id, { slug: updatedEvent.slug, name: updatedEvent.name, seatNumber: updatedEvent.seats_claimed, method: "cli", }, getClientIp(c)); if (format === "env") { return c.text( `export AUTH_YES_TOKEN="${sessionId}"\nexport AUTH_YES_USER="${username}"\n`, ); } else if (format === "json") { return c.json({ success: true, token: sessionId, username, expiresAt: expiresAt.toISOString(), }); } deleteCookie(c, "session_id", { path: "/" }); const rpID = Deno.env.get("RP_ID"); setCookie(c, "session_id", sessionId, { domain: getCookieDomain(rpID), path: "/", httpOnly: true, secure: true, sameSite: "Lax", maxAge: ttl, }); return c.redirect("/dashboard", 302); } catch (e: any) { console.error("[Events] Failed to execute CLI join:", e); return c.text("Internal Server Error", 500); } }); // --------------------------------------------------------- // Vanity Direct Workshop Entrance (/e/:slug) // --------------------------------------------------------- eventsRoutes.get("/e/:slug", async (c) => { const slug = c.req.param("slug"); try { const event = await Queries.getEventBySlug(slug); if (!event) { return c.redirect("/join?error=event_not_found", 302); } return c.html(); } catch (_e) { return c.redirect("/join?error=db_error", 302); } }); // --------------------------------------------------------- // UI Join Page // --------------------------------------------------------- eventsRoutes.get("/join", (c) => { return c.html(); });