Index: server/telefunc/ctx.ts
===================================================================
--- server/telefunc/ctx.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
+++ server/telefunc/ctx.ts	(revision c30935a5f027dac0916111525d007c79078ea267)
@@ -0,0 +1,39 @@
+import { Abort, getContext } from "telefunc";
+import * as drizzleQueries from "../../database/drizzle/queries";
+
+export function ctx() {
+    return getContext();
+}
+
+export function parseSessionUserId(sessionUserId: unknown): number | null {
+    if (typeof sessionUserId !== "string" && typeof sessionUserId !== "number") return null;
+    const n = Number(sessionUserId);
+    return Number.isInteger(n) && n > 0 ? n : null;
+}
+
+export function requireUser() {
+    const c = ctx();
+    const userId = parseSessionUserId(c.session?.user?.id);
+    if (!userId) throw Abort();
+    return { c, userId };
+}
+
+export function getAuthState() {
+    const c = ctx();
+    const userId = parseSessionUserId(c.session?.user?.id);
+    return {
+        isLoggedIn: Boolean(userId),
+        userId: userId ?? null,
+        username: c.session?.user?.name ?? null,
+        session: c.session,
+    };
+}
+
+export async function requireAdmin() {
+    const { c, userId } = requireUser();
+
+    const isAdmin = await drizzleQueries.isAdmin(c.db, userId);
+
+    if (!isAdmin) throw Abort();
+    return { c, userId };
+}
