1import "server-only";2import { cookies, headers } from "next/headers";3import { redirect } from "next/navigation";4import { cache } from "react";5import { getDb, organizations, organizationMembers, projects, eq, and, isNull, asc } from "@fetcha/db";6import type { Organization, Project } from "@fetcha/db";7import { auth, type AuthUser } from "./auth";8import { ensureWorkspace } from "./workspace";910export const PROJECT_COOKIE = "fetcha_project";1112export const getSession = cache(async () => {13 const h = await headers();14 return auth.api.getSession({ headers: h });15});1617export async function getUser(): Promise<AuthUser | null> {18 const s = await getSession();19 return s?.user ?? null;20}2122/** Redirects to /login when unauthenticated. */23export async function requireUser(next?: string): Promise<AuthUser> {24 const user = await getUser();25 if (!user) redirect(`/login${next ? `?next=${encodeURIComponent(next)}` : ""}`);26 return user;27}2829const adminEmails = (process.env.ADMIN_EMAILS ?? "")30 .split(",")31 .map((s) => s.trim().toLowerCase())32 .filter(Boolean);3334export function isAdmin(user: { email: string; role?: string | null }): boolean {35 return user.role === "admin" || adminEmails.includes(user.email.toLowerCase());36}3738export async function requireAdmin(): Promise<AuthUser> {39 const user = await requireUser("/admin");40 if (!isAdmin(user)) redirect("/dashboard");41 return user;42}4344export interface Workspace {45 user: AuthUser;46 organization: Organization;47 role: string;48 projects: Project[];49 project: Project;50 isAdmin: boolean;51}5253/** Load the user's organization, projects, and the currently selected project (cookie). */54export const getWorkspace = cache(async (): Promise<Workspace> => {55 const user = await requireUser("/dashboard");56 const db = getDb();57 let [membership] = await db58 .select({ organization: organizations, role: organizationMembers.role })59 .from(organizationMembers)60 .innerJoin(organizations, eq(organizationMembers.organizationId, organizations.id))61 .where(eq(organizationMembers.userId, user.id))62 .orderBy(asc(organizationMembers.createdAt))63 .limit(1);64 if (!membership) {65 await ensureWorkspace({ id: user.id, name: user.name, email: user.email });66 [membership] = await db67 .select({ organization: organizations, role: organizationMembers.role })68 .from(organizationMembers)69 .innerJoin(organizations, eq(organizationMembers.organizationId, organizations.id))70 .where(eq(organizationMembers.userId, user.id))71 .limit(1);72 }73 const organization = membership!.organization;74 let list = await db75 .select()76 .from(projects)77 .where(and(eq(projects.organizationId, organization.id), isNull(projects.archivedAt)))78 .orderBy(asc(projects.createdAt));79 if (!list.length) {80 await ensureWorkspace({ id: user.id, name: user.name, email: user.email });81 list = await db.select().from(projects).where(and(eq(projects.organizationId, organization.id), isNull(projects.archivedAt))).orderBy(asc(projects.createdAt));82 }83 const cookieStore = await cookies();84 const wanted = cookieStore.get(PROJECT_COOKIE)?.value;85 const project = list.find((p) => p.id === wanted) ?? list[0]!;86 return { user, organization, role: membership!.role, projects: list, project, isAdmin: isAdmin(user) };87});8889export async function requestMeta(): Promise<{ ip: string | null; userAgent: string | null }> {90 const h = await headers();91 return {92 ip: h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? h.get("x-real-ip") ?? null,93 userAgent: h.get("user-agent"),94 };95}96