SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
3.4 KB · 92 lines typescript
Raw Blame History
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