auth-yes/server/session_resolver.ts
google-labs-jules[bot] 0a4f6a8344 feat(event-controls): implement live attendee management drawer and session pause
This commit finalizes Phase 4 of the Event & Session Overhaul:
1. Implements session pause logic across PostgreSQL schema, Valkey cache, and `auth_forward.ts` edge check (`is_paused`).
2. Implements non-destructive operational endpoints (`/api/events/:id/rotate-pin`, `/api/events/:id/expand`, `/api/events/:id/attendees`) with Zero-Trust Ownership verification.
3. Upgrades existing `end` and `extend` endpoints in `events.ts` to utilize robust Zero-Trust Ownership queries (created_by OR isGlobalAdmin).
4. Creates `EventAttendeesDrawer.tsx` to handle live participant inspection and individual session controls (Pause, Revoke).
5. Updates `EventCockpitDeck.tsx` and `SessionsScript.tsx` to mount and drive the new controls via vanilla JavaScript, respecting zero-framework guidelines.
6. Ensures `deno fmt`, `deno task lint`, `deno task check` and `deno test` execute successfully against the new schema and API guards.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-27 01:53:52 +00:00

272 lines
7.3 KiB
TypeScript

import type { Context } from "jsr:@hono/hono@4";
import { deleteCookie } from "jsr:@hono/hono@4/cookie";
import { sqlWrapper } from "./db.ts";
import { valkey } from "./valkey.ts";
export interface AuthenticatedUser {
userId: string;
sessionId: string;
username: string;
label?: string;
isAgent?: boolean;
isPaused?: boolean;
customScopes?: string[];
}
export interface AppRecord {
id: string;
name: string;
domain?: string;
is_public?: boolean;
bypass_paths?: string[];
allowed_cidrs?: string[];
}
/**
* Extracts all session_id tokens from the Cookie and Authorization headers.
*/
export function extractAllSessionIds(c: Context): string[] {
const candidates: string[] = [];
// 1. Check Authorization: Bearer <token>
const authHeader = c.req.header("authorization") || "";
if (authHeader.startsWith("Bearer ")) {
const bearerToken = authHeader.substring(7).trim();
if (bearerToken) candidates.push(bearerToken);
}
// 2. Check Cookie header
const cookieHeader = c.req.header("cookie") || "";
if (cookieHeader) {
const cookieMatches = [
...cookieHeader.matchAll(/(?:^|;\s*)session_id=([^;]+)/g),
]
.map((m) => decodeURIComponent(m[1].trim()))
.filter(Boolean);
candidates.push(...cookieMatches);
}
return candidates;
}
export async function getAuthenticatedUser(
c: Context,
): Promise<AuthenticatedUser | null> {
const sessionMatches = extractAllSessionIds(c);
if (sessionMatches.length === 0) return null;
// Iterate over each candidate session ID
for (let i = 0; i < sessionMatches.length; i++) {
const candidateId = sessionMatches[i];
// 1. Try Valkey cache
try {
const sessionDataStr = await valkey.get(candidateId);
if (sessionDataStr) {
const sessionData = JSON.parse(sessionDataStr);
if (sessionData && sessionData.uuid) {
if (i > 0) {
deleteCookie(c, "session_id", { path: "/" });
}
return {
userId: sessionData.uuid,
sessionId: candidateId,
username: sessionData.username || "",
label: sessionData.label,
isAgent: sessionData.isAgent,
isPaused: sessionData.is_paused,
customScopes: sessionData.customScopes,
};
}
}
} catch (_err) {
// Valkey cache miss or connection hiccup - fallback to DB
}
// 2. Fallback to PostgreSQL sessions table
try {
const nowIso = new Date().toISOString();
const session = await sqlWrapper.sql`
SELECT s.user_id, s.expires_at, s.label, s.is_agent, s.is_paused, s.custom_scopes, u.username
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.id = ${candidateId} AND s.expires_at > ${nowIso}
`.then((res: any) => res[0]);
if (session) {
const username = session.username || "";
// Repopulate Valkey in background
try {
const ttlSeconds = Math.max(
1,
Math.floor(
(new Date(session.expires_at).getTime() - Date.now()) / 1000,
),
);
await valkey.setex(
candidateId,
ttlSeconds,
JSON.stringify({
uuid: session.user_id,
username,
label: session.label,
isAgent: session.is_agent,
is_paused: session.is_paused,
customScopes: session.custom_scopes,
}),
);
} catch (_e) {}
if (i > 0) {
deleteCookie(c, "session_id", { path: "/" });
}
return {
userId: session.user_id,
sessionId: candidateId,
username,
label: session.label,
isAgent: session.is_agent,
isPaused: session.is_paused,
customScopes: session.custom_scopes,
};
}
} catch (_err) {
// Continue to next candidate
}
}
return null;
}
/**
* Helper to get an application by host.
* Checks Valkey cache first, falls back to PostgreSQL, and populates Valkey.
*/
export async function getAppByHost(
host: string,
): Promise<AppRecord | null> {
const cacheKey = `auth:app_by_host:${host}`;
// 1. Try Valkey cache
try {
const cachedStr = await valkey.get(cacheKey);
if (cachedStr) {
return JSON.parse(cachedStr);
}
} catch (_err) {
// Valkey cache miss or connection error
}
// 2. Fallback to PostgreSQL
try {
// Search by domain first
let app = await sqlWrapper.sql`
SELECT id, name, domain, is_public, bypass_paths, allowed_cidrs
FROM apps WHERE domain = ${host}
`.then((res: any) => res[0]);
// Fallback: match name against the first subdomain segment
if (!app) {
const subdomain = host.split(".")[0];
if (subdomain) {
app = await sqlWrapper.sql`
SELECT id, name, domain, is_public, bypass_paths, allowed_cidrs
FROM apps WHERE name = ${subdomain}
`.then((res: any) => res[0]);
}
}
if (app) {
// Repopulate Valkey
try {
await valkey.setex(cacheKey, 3600, JSON.stringify(app)); // Cache for 1 hour
} catch (_e) {}
return app as AppRecord;
}
} catch (_err) {
return null;
}
return null;
}
/**
* Helper to get a user's role grant for a specific app.
* Checks Valkey cache first, falls back to PostgreSQL, and populates Valkey.
*/
export async function getUserGrant(
userId: string,
appId: string,
): Promise<string | null> {
const cacheKey = `auth:grants:${userId}:${appId}`;
// 1. Try Valkey cache
try {
const cachedRole = await valkey.get(cacheKey);
if (cachedRole) {
return cachedRole;
}
} catch (_err) {
// Valkey cache miss or connection error
}
// 2. Fallback to PostgreSQL
try {
const grant = await sqlWrapper.sql`
SELECT role FROM grants WHERE user_id = ${userId} AND app_id = ${appId}
`.then((res: any) => res[0]);
if (grant && grant.role) {
// Repopulate Valkey
try {
await valkey.setex(cacheKey, 3600, grant.role); // Cache for 1 hour
} catch (_e) {}
return grant.role;
}
} catch (_err) {
return null;
}
return null;
}
/**
* Helper to check if user has global admin privileges.
* Strict check: Requires an explicit 'admin' grant on the Management Console
* or global role, or is the bootstrap root user.
*/
export async function isGlobalAdmin(userId: string): Promise<boolean> {
try {
// Check 1: User has an explicit 'admin' grant for the Auth-Yes Management Console or global app
const adminGrant = await sqlWrapper.sql`
SELECT g.id
FROM grants g
LEFT JOIN apps a ON g.app_id = a.id
WHERE g.user_id = ${userId}
AND g.role = 'admin'
AND (
a.spiffe_id = 'spiffe://system.local/auth-yes-management'
OR a.name = 'Auth-Yes Management Console'
OR g.app_id IS NULL
)
`.then((res: any) => res[0]);
if (adminGrant) return true;
// Check 2: First registered user in system fallback
const firstUser = await sqlWrapper.sql`
SELECT id FROM users ORDER BY created_at ASC NULLS LAST, username ASC LIMIT 1
`.then((res: any) => res[0]);
if (firstUser && firstUser.id === userId) {
return true;
}
} catch (err) {
console.error("[Auth API] isGlobalAdmin error:", err);
}
return false;
}