auth-yes/ui/mod.ts

325 lines
8.7 KiB
TypeScript

import { Hono } from "jsr:@hono/hono@4";
import { serveStatic } from "jsr:@hono/hono@4/deno";
import { deleteCookie } from "jsr:@hono/hono@4/cookie";
import { sql } from "../server/db.ts";
import { valkey } from "../server/valkey.ts";
import {
extractAllSessionIds,
getAuthenticatedUser,
getCookieDomain,
isSafeRedirectUrl,
} from "../server/auth-session.ts";
import { auditWrapper } from "../server/audit.ts";
import { requireUiAuth } from "./auth_checks.ts";
import {
getAaguidAllowlist,
getAdminApps,
getAdminAuditLogs,
getAdminInvites,
getAdminRoles,
getAdminUserDetails,
getAdminUsers,
getAllRoles,
getDashboardApps,
getSessionApps,
getUserEventPasses,
getUserGrants,
getUserPasskeys,
getUserSessions,
} from "./db_queries.ts";
import { LoginPage } from "./components/LoginPage.tsx";
import { RegisterPage } from "./components/RegisterPage.tsx";
import { SessionsPage } from "./components/SessionsPage.tsx";
import { PasskeysPage } from "./components/PasskeysPage.tsx";
import { AuditLogPage } from "./components/AuditLogPage.tsx";
import { AdminUsersPage } from "./components/AdminUsersPage.tsx";
import { AdminUserDetailsPage } from "./components/AdminUserDetailsPage.tsx";
import { AAGUIDPage } from "./components/AAGUIDPage.tsx";
import { RecoveryPage } from "./components/RecoveryPage.tsx";
import { AdminAppsPage } from "./components/AdminAppsPage.tsx";
import { AdminRolesPage } from "./components/AdminRolesPage.tsx";
import { AdminInvitesPage } from "./components/AdminInvitesPage.tsx";
import { UnregisteredAppPage } from "./components/UnregisteredAppPage.tsx";
import { AppLaunchpadPage } from "./components/AppLaunchpadPage.tsx";
const uiApp: Hono = new Hono();
// Explicit Side Effect: Route rendering
uiApp.get("/", (c) => {
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
});
uiApp.get("/logout", async (c) => {
const sessionIds = extractAllSessionIds(c);
const rawRedirect = c.req.query("redirect");
let safeRedirect = null;
const userIp = c.req.header("x-forwarded-for") || "127.0.0.1";
let userId = null;
if (sessionIds.length > 0) {
try {
// Get user ID for auditing before we delete the session
const authUser = await getAuthenticatedUser(c);
if (authUser) {
userId = authUser.userId;
}
// Purge all candidate cookies sent by the browser to ensure ghosts are eradicated
for (const sId of sessionIds) {
try {
await valkey.del(sId);
await sql`DELETE FROM sessions WHERE id = ${sId}`;
} catch (_e) {}
}
} catch (_e) {
// Best effort cleanup
}
}
if (rawRedirect) {
if (isSafeRedirectUrl(rawRedirect)) {
safeRedirect = rawRedirect;
} else {
auditWrapper.auditLog(
userId,
"open_redirect_intercepted",
"logout_redirect",
{ raw_url: rawRedirect },
userIp,
);
}
}
auditWrapper.auditLog(
userId,
"logout_success",
"session",
null,
userIp,
);
const rpID = Deno.env.get("RP_ID") || "";
const cookieDomain = getCookieDomain(rpID);
if (cookieDomain) {
deleteCookie(c, "session_id", {
domain: cookieDomain,
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax",
});
}
deleteCookie(c, "session_id", {
path: "/",
httpOnly: true,
secure: true,
sameSite: "Lax",
});
if (safeRedirect) {
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect(safeRedirect);
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
});
uiApp.get("/login", (c) => {
return c.html(LoginPage());
});
uiApp.get("/errors/unregistered", (c) => {
const host = c.req.query("host") || "unknown";
return c.html(UnregisteredAppPage({ host }));
});
uiApp.get("/recovery", (c) => {
return c.html(RecoveryPage());
});
uiApp.get("/register", (c) => {
const initialCode = c.req.query("code") || "";
return c.html(RegisterPage({ initialCode }));
});
uiApp.get("/dashboard", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { auth, isAdmin } = authRes;
const apps = await getDashboardApps(auth.userId, isAdmin);
return c.html(AppLaunchpadPage({ apps: apps as any, isAdmin }));
});
uiApp.get("/dashboard/sessions", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { auth, isAdmin } = authRes;
const apps = await getSessionApps();
const sessions = await getUserSessions(auth.userId);
const eventPasses = await getUserEventPasses(auth.userId);
return c.html(
SessionsPage({
sessions,
currentSessionId: auth.sessionId,
apps: apps as any,
isAdmin,
eventPasses: eventPasses as any,
}),
);
});
uiApp.get("/dashboard/passkeys", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { auth, isAdmin } = authRes;
const passkeys = await getUserPasskeys(auth.userId);
return c.html(PasskeysPage({ passkeys, isAdmin }));
});
// Admin Routes
uiApp.get("/admin", (c) => {
return c.redirect("/admin/users");
});
uiApp.get("/admin/users", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin, isSessionAdminRole } = authRes;
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const users = await getAdminUsers();
return c.html(AdminUsersPage({ users }));
});
uiApp.get("/admin/apps", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin, isSessionAdminRole } = authRes;
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const apps = await getAdminApps();
return c.html(AdminAppsPage({ apps }));
});
uiApp.get("/admin/roles", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin, isSessionAdminRole } = authRes;
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const roles = await getAdminRoles();
const apps = await getSessionApps();
return c.html(AdminRolesPage({ roles, apps }));
});
uiApp.get("/admin/invites", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin, isSessionAdminRole } = authRes;
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const invites = await getAdminInvites();
const apps = await getSessionApps();
const allRoles = await getAllRoles();
return c.html(AdminInvitesPage({ invites, apps, allRoles }));
});
uiApp.get("/admin/aaguid", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin } = authRes;
if (!isAdmin) {
return c.redirect("/dashboard");
}
const allowlist = await getAaguidAllowlist();
return c.html(AAGUIDPage({ allowlist }));
});
uiApp.get("/admin/users/:id", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin, isSessionAdminRole } = authRes;
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/admin/users", 302);
}
const targetUserId = c.req.param("id");
const user = await getAdminUserDetails(targetUserId);
if (!user) {
return c.redirect("/admin/users");
}
const sessions = await getUserSessions(targetUserId);
const passkeys = await getUserPasskeys(targetUserId);
const grants = await getUserGrants(targetUserId);
const allApps = await getSessionApps();
const allRoles = await getAllRoles();
return c.html(
AdminUserDetailsPage({
user,
sessions,
passkeys,
grants,
allApps,
allRoles,
}),
);
});
uiApp.get("/admin/audit-logs", async (c) => {
const authRes = await requireUiAuth(c);
if (authRes instanceof Response) return authRes;
const { isAdmin, isSessionAdminRole } = authRes;
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const logs = await getAdminAuditLogs();
return c.html(AuditLogPage({ logs }));
});
// Explicit Side Effect: Serving static assets (client-side JS) with no-cache headers to prevent stale browser caches
uiApp.use("/public/*", async (c, next) => {
await next();
c.header("Cache-Control", "no-cache, no-store, must-revalidate");
});
uiApp.get(
"/public/*",
serveStatic({
root: "./ui",
rewriteRequestPath: (path) => path.replace(/^\/public/, "/public"),
}),
);
export { uiApp };