TypeScript 97.5%
SQL 1.4%
Python 0.8%
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";9import { isAdminEmail } from "./access";1011export const PROJECT_COOKIE = "fetcha_project";1213export const getSession = cache(async () => {14 const h = await headers();15 return auth.api.getSession({ headers: h });16});1718export async function getUser(): Promise<AuthUser | null> {19 const s = await getSession();20 return s?.user ?? null;21}2223/** Redirects to /login when unauthenticated. */24export async function requireUser(next?: string): Promise<AuthUser> {25 const user = await getUser();26 if (!user) redirect(`/login${next ? `?next=${encodeURIComponent(next)}` : ""}`);27 return user;28}2930export function isAdmin(user: { email: string; role?: string | null }): boolean {31 return user.role === "admin" || isAdminEmail(user.email);32}3334export async function requireAdmin(): Promise<AuthUser> {35 const user = await requireUser("/admin");36 if (!isAdmin(user)) redirect("/dashboard");37 return user;38}3940export interface Workspace {41 user: AuthUser;42 organization: Organization;43 role: string;44 projects: Project[];45 project: Project;46 isAdmin: boolean;47}4849/** Load the user's organization, projects, and the currently selected project (cookie). */50export const getWorkspace = cache(async (): Promise<Workspace> => {51 const user = await requireUser("/dashboard");52 const db = getDb();53 let [membership] = await db54 .select({ organization: organizations, role: organizationMembers.role })55 .from(organizationMembers)56 .innerJoin(organizations, eq(organizationMembers.organizationId, organizations.id))57 .where(eq(organizationMembers.userId, user.id))58 .orderBy(asc(organizationMembers.createdAt))59 .limit(1);60 if (!membership) {61 await ensureWorkspace({ id: user.id, name: user.name, email: user.email });62 [membership] = await db63 .select({ organization: organizations, role: organizationMembers.role })64 .from(organizationMembers)65 .innerJoin(organizations, eq(organizationMembers.organizationId, organizations.id))66 .where(eq(organizationMembers.userId, user.id))67 .limit(1);68 }69 const organization = membership!.organization;70 let list = await db71 .select()72 .from(projects)73 .where(and(eq(projects.organizationId, organization.id), isNull(projects.archivedAt)))74 .orderBy(asc(projects.createdAt));75 if (!list.length) {76 await ensureWorkspace({ id: user.id, name: user.name, email: user.email });77 list = await db.select().from(projects).where(and(eq(projects.organizationId, organization.id), isNull(projects.archivedAt))).orderBy(asc(projects.createdAt));78 }79 const cookieStore = await cookies();80 const wanted = cookieStore.get(PROJECT_COOKIE)?.value;81 const project = list.find((p) => p.id === wanted) ?? list[0]!;82 return { user, organization, role: membership!.role, projects: list, project, isAdmin: isAdmin(user) };83});8485export async function requestMeta(): Promise<{ ip: string | null; userAgent: string | null }> {86 const h = await headers();87 return {88 ip: h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? h.get("x-real-ip") ?? null,89 userAgent: h.get("user-agent"),90 };91}92