import "server-only"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { cache } from "react"; import { getDb, organizations, organizationMembers, projects, eq, and, isNull, asc } from "@fetcha/db"; import type { Organization, Project } from "@fetcha/db"; import { auth, type AuthUser } from "./auth"; import { ensureWorkspace } from "./workspace"; import { isAdminEmail } from "./access"; export const PROJECT_COOKIE = "fetcha_project"; export const getSession = cache(async () => { const h = await headers(); return auth.api.getSession({ headers: h }); }); export async function getUser(): Promise { const s = await getSession(); return s?.user ?? null; } /** Redirects to /login when unauthenticated. */ export async function requireUser(next?: string): Promise { const user = await getUser(); if (!user) redirect(`/login${next ? `?next=${encodeURIComponent(next)}` : ""}`); return user; } export function isAdmin(user: { email: string; role?: string | null }): boolean { return user.role === "admin" || isAdminEmail(user.email); } export async function requireAdmin(): Promise { const user = await requireUser("/admin"); if (!isAdmin(user)) redirect("/dashboard"); return user; } export interface Workspace { user: AuthUser; organization: Organization; role: string; projects: Project[]; project: Project; isAdmin: boolean; } /** Load the user's organization, projects, and the currently selected project (cookie). */ export const getWorkspace = cache(async (): Promise => { const user = await requireUser("/dashboard"); const db = getDb(); let [membership] = await db .select({ organization: organizations, role: organizationMembers.role }) .from(organizationMembers) .innerJoin(organizations, eq(organizationMembers.organizationId, organizations.id)) .where(eq(organizationMembers.userId, user.id)) .orderBy(asc(organizationMembers.createdAt)) .limit(1); if (!membership) { await ensureWorkspace({ id: user.id, name: user.name, email: user.email }); [membership] = await db .select({ organization: organizations, role: organizationMembers.role }) .from(organizationMembers) .innerJoin(organizations, eq(organizationMembers.organizationId, organizations.id)) .where(eq(organizationMembers.userId, user.id)) .limit(1); } const organization = membership!.organization; let list = await db .select() .from(projects) .where(and(eq(projects.organizationId, organization.id), isNull(projects.archivedAt))) .orderBy(asc(projects.createdAt)); if (!list.length) { await ensureWorkspace({ id: user.id, name: user.name, email: user.email }); list = await db.select().from(projects).where(and(eq(projects.organizationId, organization.id), isNull(projects.archivedAt))).orderBy(asc(projects.createdAt)); } const cookieStore = await cookies(); const wanted = cookieStore.get(PROJECT_COOKIE)?.value; const project = list.find((p) => p.id === wanted) ?? list[0]!; return { user, organization, role: membership!.role, projects: list, project, isAdmin: isAdmin(user) }; }); export async function requestMeta(): Promise<{ ip: string | null; userAgent: string | null }> { const h = await headers(); return { ip: h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? h.get("x-real-ip") ?? null, userAgent: h.get("user-agent"), }; }