SPB Git

spb/fetcha

Public
3.5 KB · 96 lines typescript
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
Files · 6b215c2d5f54167737c2d57307f22c84bc934434
Menu

Theme