auth-yes/ui/mod.ts

530 lines
15 KiB
TypeScript

import { Hono } from "jsr:@hono/hono@4";
import { serveStatic } from "jsr:@hono/hono@4/deno";
import { deleteCookie, getCookie } from "jsr:@hono/hono@4/cookie";
import { sql } from "../server/db.ts";
import { valkey } from "../server/valkey.ts";
import {
extractAllSessionIds,
getAuthenticatedUser,
getCookieDomain,
isGlobalAdmin,
isSafeRedirectUrl,
isSessionAdmin,
} from "../server/auth-session.ts";
import { auditWrapper } from "../server/audit.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 auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
let apps = [];
if (isAdmin) {
apps = await sql`
SELECT id, name, description, domain, 'Admin' as role
FROM apps
WHERE domain IS NOT NULL
ORDER BY name ASC
` as any[];
} else {
apps = await sql`
SELECT a.id, a.name, a.description, a.domain, g.role
FROM apps a
JOIN grants g ON a.id = g.app_id
WHERE g.user_id = ${auth.userId} AND a.domain IS NOT NULL
ORDER BY a.name ASC
` as any[];
}
return c.html(AppLaunchpadPage({ apps: apps as any, isAdmin }));
});
uiApp.get("/dashboard/sessions", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const apps = await sql`
SELECT id, name, domain, spiffe_id
FROM apps
ORDER BY name ASC
`;
const sessions = await sql`
SELECT id, label, is_agent, custom_scopes, last_activity_at, last_activity_action, created_at, expires_at
FROM sessions
WHERE user_id = ${auth.userId} AND expires_at > NOW()
ORDER BY created_at DESC
`;
const eventPasses = await sql`
SELECT id, slug, pin_code, name, max_seats, seats_claimed, is_active, expires_at
FROM event_passes
WHERE created_by = ${auth.userId} AND is_active = TRUE
ORDER BY created_at DESC
`;
return c.html(
SessionsPage({
sessions,
currentSessionId: auth.sessionId,
apps: apps as any,
isAdmin,
eventPasses: eventPasses as any,
}),
);
});
uiApp.get("/dashboard/passkeys", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const passkeys = await sql`
SELECT id, credential_id, counter
FROM passkeys
WHERE user_id = ${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 auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth);
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const users = await sql`
SELECT id, username, display_name, account_status
FROM users
ORDER BY username ASC
`;
return c.html(AdminUsersPage({ users }));
});
uiApp.get("/admin/apps", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth);
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const apps = await sql`
SELECT a.id, a.name, a.spiffe_id, a.description, a.created_at,
COUNT(g.id) AS active_grants_count
FROM apps a
LEFT JOIN grants g ON a.id = g.app_id
GROUP BY a.id, a.name, a.spiffe_id, a.description, a.created_at
ORDER BY a.created_at ASC
`;
return c.html(AdminAppsPage({ apps }));
});
uiApp.get("/admin/roles", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth);
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const roles = await sql`
SELECT r.id, r.name, r.description, r.app_id, r.created_at,
a.name AS app_name
FROM roles r
LEFT JOIN apps a ON r.app_id = a.id
ORDER BY r.app_id NULLS FIRST, r.name ASC
`;
const apps = await sql`
SELECT id, name, spiffe_id FROM apps ORDER BY name ASC
`;
return c.html(AdminRolesPage({ roles, apps }));
});
uiApp.get("/admin/invites", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth);
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const invites = await sql`
SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at,
a.name AS app_name, a.id AS app_id,
u.username AS used_by_username
FROM invites i
LEFT JOIN apps a ON i.app_id = a.id
LEFT JOIN users u ON i.used_by = u.id
ORDER BY i.created_at DESC
`;
const apps = await sql`
SELECT id, name, spiffe_id FROM apps ORDER BY name ASC
`;
const allRoles = await sql`
SELECT id, name, description, app_id FROM roles ORDER BY name ASC
`;
return c.html(AdminInvitesPage({ invites, apps, allRoles }));
});
uiApp.get("/admin/aaguid", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
if (!isAdmin) {
return c.redirect("/dashboard");
}
const allowlist = await sql`
SELECT id, aaguid, description, created_at
FROM aaguid_allowlist
ORDER BY created_at DESC
`;
return c.html(AAGUIDPage({ allowlist }));
});
uiApp.get("/admin/users/:id", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth);
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/admin/users", 302);
}
const targetUserId = c.req.param("id");
const user = await sql`
SELECT id, username, display_name, account_status
FROM users
WHERE id = ${targetUserId}
`.then((res) => res[0]);
if (!user) {
return c.redirect("/admin/users");
}
const sessions = await sql`
SELECT id, created_at, expires_at
FROM sessions
WHERE user_id = ${targetUserId} AND expires_at > NOW()
ORDER BY created_at DESC
`;
const passkeys = await sql`
SELECT id, credential_id, counter
FROM passkeys
WHERE user_id = ${targetUserId}
`;
const grants = await sql`
SELECT g.id, g.app_id, g.role, g.created_at, a.name AS app_name, a.spiffe_id
FROM grants g
JOIN apps a ON g.app_id = a.id
WHERE g.user_id = ${targetUserId}
ORDER BY a.name ASC
`;
const allApps = await sql`
SELECT id, name, spiffe_id FROM apps ORDER BY name ASC
`;
const allRoles = await sql`
SELECT id, name, description, app_id FROM roles ORDER BY name ASC
`;
return c.html(
AdminUserDetailsPage({
user,
sessions,
passkeys,
grants,
allApps,
allRoles,
}),
);
});
uiApp.get("/admin/audit-logs", async (c) => {
const auth = await getAuthenticatedUser(c);
if (!auth) {
if (getCookie(c, "session_id")) {
deleteCookie(c, "session_id", { path: "/" }); // clear host cookie
deleteCookie(c, "session_id", {
domain: getCookieDomain(Deno.env.get("RP_ID")),
path: "/",
}); // clear domain cookie
}
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
return c.redirect("/login");
}
const isAdmin = await isGlobalAdmin(auth.userId);
const isSessionAdminRole = await isSessionAdmin(auth);
if (!isAdmin || !isSessionAdminRole) {
return c.redirect("/dashboard", 302);
}
const logs = await sql`
SELECT a.id, a.action, a.resource, a.details, a.ip_address, a.created_at, u.username as user
FROM audit_records a
LEFT JOIN users u ON a.user_id = u.id
ORDER BY a.created_at DESC
LIMIT 100
`;
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 };