638 lines
19 KiB
TypeScript
638 lines
19 KiB
TypeScript
import { Hono } from "jsr:@hono/hono@4";
|
|
import { deleteCookie, setCookie } from "jsr:@hono/hono@4/cookie";
|
|
import { encodeHex } from "jsr:@std/encoding@1/hex";
|
|
import { sqlWrapper } from "../db.ts";
|
|
import { valkey } from "../valkey.ts";
|
|
import {
|
|
getAuthenticatedUser,
|
|
getCookieDomain,
|
|
hasScope,
|
|
isGlobalAdmin,
|
|
} from "../auth-session.ts";
|
|
import { getClientIp } from "../middleware.ts";
|
|
import { auditWrapper } from "../audit.ts";
|
|
import { rateLimitWrapper } from "../ratelimit.ts";
|
|
import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx";
|
|
import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx";
|
|
|
|
export const eventRoutes = new Hono();
|
|
|
|
// ---------------------------------------------------------
|
|
// Multi-Claim Event Passes & Short Code Join Portal
|
|
// ---------------------------------------------------------
|
|
|
|
eventRoutes.post("/api/events/:id/rotate-pin", async (c) => {
|
|
const user = await getAuthenticatedUser(c);
|
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
if (!hasScope(user, "write:events")) {
|
|
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
|
|
}
|
|
|
|
const eventId = c.req.param("id");
|
|
|
|
try {
|
|
const event = await sqlWrapper.sql`
|
|
SELECT slug, name FROM event_passes
|
|
WHERE id = ${eventId}
|
|
AND (created_by = ${user.userId} OR ${await isGlobalAdmin(user.userId)})
|
|
AND is_active = TRUE
|
|
`;
|
|
|
|
if (!event || event.length === 0) {
|
|
return c.json(
|
|
{ error: "Event not found, inactive, or unauthorized" },
|
|
404,
|
|
);
|
|
}
|
|
|
|
// Generate new PIN
|
|
const randPin = Math.floor(100000 + Math.random() * 900000).toString();
|
|
const newPinCode = randPin.substring(0, 3) + "-" + randPin.substring(3);
|
|
|
|
// Generate new Slug
|
|
const baseSlug = event[0].slug.replace(/-[a-z0-9]{4}$/, "");
|
|
const newSuffix = Math.random().toString(36).substring(2, 6);
|
|
const newSlug = `${baseSlug}-${newSuffix}`;
|
|
|
|
const updateResult = await sqlWrapper.sql`
|
|
UPDATE event_passes
|
|
SET pin_code = ${newPinCode}, slug = ${newSlug}
|
|
WHERE id = ${eventId}
|
|
RETURNING pin_code, slug
|
|
`;
|
|
|
|
auditWrapper.auditLog(
|
|
user.userId,
|
|
"event_ingress_rotated",
|
|
eventId,
|
|
{},
|
|
getClientIp(c),
|
|
);
|
|
|
|
return c.json({
|
|
success: true,
|
|
pinCode: updateResult[0].pin_code,
|
|
slug: updateResult[0].slug,
|
|
});
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to rotate event PIN:", e);
|
|
return c.json({ error: "Failed to rotate PIN" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.post("/api/events/:id/expand", async (c) => {
|
|
const user = await getAuthenticatedUser(c);
|
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
if (!hasScope(user, "write:events")) {
|
|
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
|
|
}
|
|
|
|
const eventId = c.req.param("id");
|
|
const body = await c.req.json().catch(() => ({}));
|
|
const addSeats = Math.max(Number(body.addSeats) || 5, 1);
|
|
|
|
try {
|
|
const eventResult = await sqlWrapper.sql`
|
|
UPDATE event_passes
|
|
SET max_seats = max_seats + ${addSeats}
|
|
WHERE id = ${eventId}
|
|
AND (created_by = ${user.userId} OR ${await isGlobalAdmin(user.userId)})
|
|
AND is_active = TRUE
|
|
RETURNING max_seats
|
|
`;
|
|
|
|
if (!eventResult || eventResult.length === 0) {
|
|
return c.json(
|
|
{ error: "Event not found, inactive, or unauthorized" },
|
|
404,
|
|
);
|
|
}
|
|
|
|
auditWrapper.auditLog(user.userId, "event_seats_expanded", eventId, {
|
|
added: addSeats,
|
|
newMax: eventResult[0].max_seats,
|
|
}, getClientIp(c));
|
|
|
|
return c.json({ success: true, maxSeats: eventResult[0].max_seats });
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to expand event seats:", e);
|
|
return c.json({ error: "Failed to expand seats" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.get("/api/events/:id/attendees", async (c) => {
|
|
const user = await getAuthenticatedUser(c);
|
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
if (!hasScope(user, "read:events")) {
|
|
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
|
|
}
|
|
|
|
const eventId = c.req.param("id");
|
|
|
|
try {
|
|
const event = await sqlWrapper.sql`
|
|
SELECT id, slug, max_seats, expires_at FROM event_passes
|
|
WHERE id = ${eventId}
|
|
AND (created_by = ${user.userId} OR ${await isGlobalAdmin(user.userId)})
|
|
`.then((res: any) => res[0]);
|
|
|
|
if (!event) {
|
|
return c.json({ error: "Event not found or unauthorized" }, 404);
|
|
}
|
|
|
|
const attendees = await sqlWrapper.sql`
|
|
SELECT s.id, s.label, s.is_paused, s.created_at, s.expires_at, s.last_activity_at, s.last_activity_action, u.username, u.display_name
|
|
FROM sessions s
|
|
JOIN users u ON s.user_id = u.id
|
|
WHERE u.event_pass_id = ${eventId}
|
|
ORDER BY s.created_at DESC
|
|
`;
|
|
|
|
return c.json({
|
|
success: true,
|
|
attendees,
|
|
event: {
|
|
max_seats: event.max_seats,
|
|
expires_at: event.expires_at,
|
|
},
|
|
});
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to fetch attendees:", e);
|
|
return c.json({ error: "Failed to fetch attendees" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.post("/api/events/:id/end", async (c) => {
|
|
const user = await getAuthenticatedUser(c);
|
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
if (!hasScope(user, "write:events")) {
|
|
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
|
|
}
|
|
|
|
const eventId = c.req.param("id");
|
|
|
|
try {
|
|
const eventResult = await sqlWrapper.sql`
|
|
UPDATE event_passes
|
|
SET is_active = FALSE
|
|
WHERE id = ${eventId} AND (created_by = ${user.userId} OR ${await isGlobalAdmin(
|
|
user.userId,
|
|
)})
|
|
RETURNING id
|
|
`;
|
|
|
|
if (!eventResult || eventResult.length === 0) {
|
|
return c.json({ error: "Event not found or unauthorized" }, 404);
|
|
}
|
|
|
|
const sessionResult = await sqlWrapper.sql`
|
|
DELETE FROM sessions
|
|
WHERE user_id IN (
|
|
SELECT id FROM users WHERE event_pass_id = ${eventId}
|
|
)
|
|
RETURNING id
|
|
`;
|
|
|
|
const sessionIds = sessionResult.map((s: any) => s.id);
|
|
if (sessionIds.length > 0) {
|
|
await valkey.del(...sessionIds);
|
|
}
|
|
|
|
return c.json({ success: true, revokedCount: sessionIds.length });
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to end event:", e);
|
|
return c.json({ error: "Failed to end event" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.post("/api/events/:id/extend", async (c) => {
|
|
const user = await getAuthenticatedUser(c);
|
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
if (!hasScope(user, "write:events")) {
|
|
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
|
|
}
|
|
|
|
const eventId = c.req.param("id");
|
|
const body = await c.req.json().catch(() => ({}));
|
|
const extendHours = Math.max(Number(body.extendHours) || 1, 1);
|
|
|
|
try {
|
|
const eventResult = await sqlWrapper.sql`
|
|
UPDATE event_passes
|
|
SET expires_at = GREATEST(expires_at, NOW()) + INTERVAL '1 hour' * ${extendHours}
|
|
WHERE id = ${eventId} AND (created_by = ${user.userId} OR ${await isGlobalAdmin(
|
|
user.userId,
|
|
)}) AND is_active = TRUE
|
|
RETURNING id, expires_at
|
|
`;
|
|
|
|
if (!eventResult || eventResult.length === 0) {
|
|
return c.json(
|
|
{ error: "Event not found, inactive, or unauthorized" },
|
|
404,
|
|
);
|
|
}
|
|
|
|
const newExpiresAt = new Date(eventResult[0].expires_at);
|
|
|
|
const sessionResult = await sqlWrapper.sql`
|
|
UPDATE sessions
|
|
SET expires_at = expires_at + INTERVAL '1 hour' * ${extendHours}
|
|
WHERE user_id IN (
|
|
SELECT id FROM users WHERE event_pass_id = ${eventId}
|
|
)
|
|
RETURNING id
|
|
`;
|
|
|
|
const sessionIds = sessionResult.map((s: any) => s.id);
|
|
if (sessionIds.length > 0) {
|
|
const ttlSeconds = Math.max(
|
|
1,
|
|
Math.floor((newExpiresAt.getTime() - Date.now()) / 1000),
|
|
);
|
|
for (const sid of sessionIds) {
|
|
await valkey.expire(sid, ttlSeconds);
|
|
}
|
|
}
|
|
|
|
return c.json({ success: true, expiresAt: newExpiresAt.toISOString() });
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to extend event:", e);
|
|
return c.json({ error: "Failed to extend event" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.post("/api/events", async (c) => {
|
|
const user = await getAuthenticatedUser(c);
|
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
if (!hasScope(user, "write:events")) {
|
|
return c.json({ error: "Forbidden: Insufficient scopes" }, 403);
|
|
}
|
|
|
|
const body = await c.req.json().catch(() => ({}));
|
|
const { name, appId, role = "viewer", maxSeats = 50, lifespanHours = 3 } =
|
|
body;
|
|
let { slug, pinCode } = body;
|
|
|
|
if (!name || typeof name !== "string") {
|
|
return c.json({ error: "Event name is required" }, 400);
|
|
}
|
|
|
|
if (!slug) {
|
|
slug =
|
|
name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") +
|
|
"-" + Math.random().toString(36).substring(2, 6);
|
|
}
|
|
if (!pinCode) {
|
|
const randPin = Math.floor(100000 + Math.random() * 900000).toString();
|
|
pinCode = randPin.substring(0, 3) + "-" + randPin.substring(3);
|
|
}
|
|
|
|
try {
|
|
const expiresAt = new Date(
|
|
Date.now() + Number(lifespanHours) * 3600 * 1000,
|
|
);
|
|
const result = await sqlWrapper.sql`
|
|
INSERT INTO event_passes (slug, pin_code, name, app_id, role, max_seats, lifespan_hours, created_by, expires_at)
|
|
VALUES (${slug}, ${pinCode}, ${name}, ${appId || null}, ${role}, ${
|
|
Number(maxSeats)
|
|
}, ${Number(lifespanHours)}, ${user.userId}, ${expiresAt})
|
|
RETURNING *
|
|
`;
|
|
return c.json({ success: true, event: result[0] });
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to create event pass:", e);
|
|
return c.json({ error: "Failed to create event pass" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.post("/api/join", async (c) => {
|
|
let code = "";
|
|
if (
|
|
c.req.header("content-type")?.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") {
|
|
return c.json({ error: "Event code or PIN is required" }, 400);
|
|
}
|
|
|
|
const rawNormalized = code.trim().toLowerCase();
|
|
const pinNormalized = code.trim().replace(/[-\s]/g, "");
|
|
|
|
const clientIp = getClientIp(c);
|
|
const rateLimitKey = `ratelimit:join:fail:${clientIp}`;
|
|
|
|
if (await rateLimitWrapper.isRateLimited(rateLimitKey, 5, 60000)) {
|
|
return c.json({ error: "Too Many Requests" }, 429);
|
|
}
|
|
|
|
try {
|
|
const eventLookup = await sqlWrapper.sql`
|
|
SELECT * FROM event_passes
|
|
WHERE (LOWER(slug) = ${rawNormalized} OR REPLACE(pin_code, '-', '') = ${pinNormalized})
|
|
AND is_active = TRUE
|
|
AND (expires_at IS NULL OR expires_at > NOW())
|
|
`;
|
|
|
|
if (!eventLookup || eventLookup.length === 0) {
|
|
await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000);
|
|
return c.json(
|
|
{ error: "Invalid event code or workshop capacity reached" },
|
|
404,
|
|
);
|
|
}
|
|
|
|
const event = eventLookup[0];
|
|
const user = await getAuthenticatedUser(c);
|
|
|
|
if (user && user.username?.startsWith(`guest_${event.slug}_`)) {
|
|
const ttl = (Number(event.lifespan_hours) || 3) * 3600;
|
|
await valkey.expire(user.sessionId, ttl);
|
|
|
|
deleteCookie(c, "session_id", { path: "/" });
|
|
const rpID = Deno.env.get("RP_ID");
|
|
const cookieDomain = getCookieDomain(rpID);
|
|
|
|
setCookie(c, "session_id", user.sessionId, {
|
|
domain: cookieDomain,
|
|
path: "/",
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: "Lax",
|
|
maxAge: ttl,
|
|
});
|
|
|
|
let appDomain = "";
|
|
if (event.app_id) {
|
|
const apps = await sqlWrapper
|
|
.sql`SELECT domain FROM apps WHERE id = ${event.app_id}`;
|
|
if (apps.length > 0) {
|
|
appDomain = apps[0].domain || "";
|
|
}
|
|
}
|
|
const redirectUrl = appDomain ? `https://${appDomain}` : "/dashboard";
|
|
|
|
if (
|
|
c.req.header("accept")?.includes("text/html") &&
|
|
!c.req.header("accept")?.includes("application/json")
|
|
) {
|
|
return c.redirect(redirectUrl, 302);
|
|
}
|
|
|
|
return c.json({
|
|
success: true,
|
|
sessionId: user.sessionId,
|
|
token: user.sessionId,
|
|
guestUuid: user.userId,
|
|
username: user.username,
|
|
redirectUrl,
|
|
reused: true,
|
|
});
|
|
}
|
|
|
|
const updateResult = await sqlWrapper.sql`
|
|
UPDATE event_passes
|
|
SET seats_claimed = seats_claimed + 1
|
|
WHERE id = ${event.id}
|
|
AND (max_seats = 0 OR seats_claimed < max_seats)
|
|
RETURNING *
|
|
`;
|
|
|
|
if (!updateResult || updateResult.length === 0) {
|
|
await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 60000);
|
|
return c.json(
|
|
{ error: "Workshop capacity reached" },
|
|
404,
|
|
);
|
|
}
|
|
|
|
const updatedEvent = updateResult[0];
|
|
const guestUuid = crypto.randomUUID();
|
|
const eventShortId = String(updatedEvent.id).split("-")[0];
|
|
const username = `guest_${eventShortId}_${updatedEvent.seats_claimed}`;
|
|
|
|
await sqlWrapper.sql`
|
|
INSERT INTO users (id, username, display_name, account_status, event_pass_id)
|
|
VALUES (${guestUuid}, ${username}, ${
|
|
updatedEvent.name + " Attendee"
|
|
}, 'guest', ${updatedEvent.id})
|
|
ON CONFLICT DO NOTHING
|
|
`;
|
|
|
|
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 apps = await sqlWrapper
|
|
.sql`SELECT name, domain FROM apps WHERE id = ${updatedEvent.app_id}`;
|
|
if (apps.length > 0) {
|
|
customScopes = [`app:${apps[0].name}`, updatedEvent.role || "viewer"];
|
|
appDomain = apps[0].domain || "";
|
|
}
|
|
}
|
|
|
|
const expiresAt = new Date(Date.now() + ttl * 1000);
|
|
|
|
await sqlWrapper.sql`
|
|
INSERT INTO sessions (id, user_id, label, is_agent, custom_scopes, expires_at)
|
|
VALUES (${sessionId}, ${guestUuid}, ${label}, false, ${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 (
|
|
c.req.header("accept")?.includes("text/html") &&
|
|
!c.req.header("accept")?.includes("application/json")
|
|
) {
|
|
return c.redirect(redirectUrl, 302);
|
|
}
|
|
|
|
return c.json({
|
|
success: true,
|
|
sessionId,
|
|
token: sessionId,
|
|
guestUuid,
|
|
username,
|
|
redirectUrl,
|
|
});
|
|
} catch (e: any) {
|
|
console.error("[Events] Failed to join event:", e);
|
|
return c.json({ error: "Failed to join event" }, 500);
|
|
}
|
|
});
|
|
|
|
eventRoutes.get("/join/:slug", async (c) => {
|
|
const slug = c.req.param("slug") || "";
|
|
const format = c.req.query("format") || "html";
|
|
|
|
const rawNormalized = slug.trim().toLowerCase();
|
|
const pinNormalized = slug.trim().replace(/[-\s]/g, "");
|
|
|
|
try {
|
|
const result = await sqlWrapper.sql`
|
|
UPDATE event_passes
|
|
SET seats_claimed = seats_claimed + 1
|
|
WHERE (LOWER(slug) = ${rawNormalized} OR REPLACE(pin_code, '-', '') = ${pinNormalized})
|
|
AND is_active = TRUE
|
|
AND (expires_at IS NULL OR expires_at > NOW())
|
|
AND (max_seats = 0 OR seats_claimed < max_seats)
|
|
RETURNING *
|
|
`;
|
|
|
|
if (!result || result.length === 0) {
|
|
if (format === "env" || format === "json") {
|
|
return c.text("Invalid slug or workshop capacity reached", 404);
|
|
}
|
|
return c.redirect("/join?error=not_found", 302);
|
|
}
|
|
|
|
const event = result[0];
|
|
const guestUuid = crypto.randomUUID();
|
|
const eventShortId = String(event.id).split("-")[0];
|
|
const username = `guest_${eventShortId}_${event.seats_claimed}`;
|
|
|
|
await sqlWrapper.sql`
|
|
INSERT INTO users (id, username, display_name, account_status, event_pass_id)
|
|
VALUES (${guestUuid}, ${username}, ${
|
|
event.name + " Attendee"
|
|
}, 'guest', ${event.id})
|
|
ON CONFLICT DO NOTHING
|
|
`;
|
|
|
|
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
const sessionId = `ay_sess_${encodeHex(randomBytes)}`;
|
|
const label = `${event.name} Seat #${event.seats_claimed}`;
|
|
const ttl = (Number(event.lifespan_hours) || 3) * 3600;
|
|
|
|
let customScopes = ["guest", "trial"];
|
|
if (event.app_id) {
|
|
const apps = await sqlWrapper
|
|
.sql`SELECT name, domain FROM apps WHERE id = ${event.app_id}`;
|
|
if (apps.length > 0) {
|
|
customScopes = [`app:${apps[0].name}`, event.role || "viewer"];
|
|
}
|
|
}
|
|
|
|
const expiresAt = new Date(Date.now() + ttl * 1000);
|
|
|
|
await sqlWrapper.sql`
|
|
INSERT INTO sessions (id, user_id, label, is_agent, custom_scopes, expires_at)
|
|
VALUES (${sessionId}, ${guestUuid}, ${label}, false, ${customScopes}, ${expiresAt})
|
|
`;
|
|
|
|
await valkey.setex(
|
|
sessionId,
|
|
ttl,
|
|
JSON.stringify({
|
|
uuid: guestUuid,
|
|
username,
|
|
account_status: "guest",
|
|
customScopes,
|
|
}),
|
|
);
|
|
|
|
auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, {
|
|
slug: event.slug,
|
|
name: event.name,
|
|
seatNumber: event.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);
|
|
}
|
|
});
|
|
|
|
eventRoutes.get("/join", (c) => {
|
|
return c.html(EventJoinPage());
|
|
});
|
|
|
|
eventRoutes.get("/e/:slug", async (c) => {
|
|
const slug = c.req.param("slug");
|
|
try {
|
|
const result = await sqlWrapper.sql`
|
|
SELECT * FROM event_passes WHERE slug = ${slug} AND is_active = TRUE
|
|
`;
|
|
if (!result || result.length === 0) {
|
|
return c.redirect("/join?error=event_not_found", 302);
|
|
}
|
|
return c.html(EventSplashPage({ event: result[0] }));
|
|
} catch (_e) {
|
|
return c.redirect("/join?error=db_error", 302);
|
|
}
|
|
});
|