import "server-only"; import { eq } from "drizzle-orm"; import { cookies } from "next/headers"; import { getSession } from "@/lib/auth"; import { withPlatformAdmin } from "@/db/client"; import { adminUsers, adminUserBrands, brands } from "@/db/schema"; import type { AdminRole, AdminUser, TenantContext } from "@/lib/admin-permissions-types"; /** * Source of truth for the current admin user. * * Looks up the Neon Auth session, then resolves the user from the * `admin_users` table (linked to `neon_auth.user` by email). * * In development mode (NODE_ENV !== "production"), also checks for * dev_session cookie as a bypass for local testing. * * Returns `null` if: * - No Neon Auth session (caller not signed in) * - The session email doesn't match any `admin_users.email` * - The user has no `admin_user_brands` row (not provisioned yet) * * Provisioning: an admin must run * INSERT INTO admin_users (email, ...) VALUES (...) * INSERT INTO admin_user_brands (admin_user_id, brand_id, role) VALUES (...) * to grant a signed-in user admin access. Until provisioned, the * layout shows "Access Denied" — correct behavior. */ export async function getAdminUser(): Promise { // Check for dev_session cookie in development mode if (process.env.NODE_ENV !== "production") { const cookieStore = await cookies(); const devSession = cookieStore.get("dev_session")?.value; if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") { return buildDevAdmin(devSession as AdminRole); } } let sessionEmail: string | null = null; try { const { data: session } = await getSession(); sessionEmail = session?.user?.email ?? null; } catch (err) { console.error("[admin-permissions] getSession() failed:", err); return null; } if (!sessionEmail) return null; try { return await withPlatformAdmin(async (db) => { const userRows = await db .select() .from(adminUsers) .where(eq(adminUsers.email, sessionEmail.toLowerCase())) .limit(1); const user = userRows[0]; if (!user) return null; const membershipRows = await db .select({ brandId: adminUserBrands.brandId, brandName: brands.name, brandSlug: brands.slug, // Role comes from admin_users table, not admin_user_brands role: adminUsers.role, }) .from(adminUserBrands) .innerJoin(brands, eq(brands.id, adminUserBrands.brandId)) .innerJoin(adminUsers, eq(adminUsers.id, adminUserBrands.adminUserId)) .where(eq(adminUserBrands.adminUserId, user.id)) .limit(1); if (membershipRows.length === 0) { // Signed in but not provisioned for any brand. return null; } const m = membershipRows[0]; const role = user.role as AdminRole; return buildAdminUser({ id: user.id, email: user.email, displayName: user.name, brandId: m.brandId, brandName: m.brandName, brandSlug: m.brandSlug, role, active: true, }); }); } catch (err) { console.error("[admin-permissions] Database query failed:", err); return null; } } /** * Resolves the current admin user AND their brand. Returns `null` if * the user is not signed in or has no brand. For platform_admin (no * brand), `brand` is `null` and callers should use `withPlatformAdmin` * to query across all brands. */ export async function getCurrentTenant(): Promise { const user = await getAdminUser(); if (!user) return null; if (!user.brand_id) { // platform_admin — no specific brand return null; } return { user, tenant: { id: user.brand_id, name: user.display_name ?? user.brand_slug ?? "Unknown", slug: user.brand_slug ?? "unknown", status: "active", }, }; } // ──────────────────────────────────────────────────────────────────────── // Re-exports for backward compat // ──────────────────────────────────────────────────────────────────────── export type { AdminUser, AdminRole, TenantContext } from "@/lib/admin-permissions-types"; /** * @deprecated Kept for unit tests that exercise the dev shim path. * Production code should never call this — `getAdminUser()` only reads * the Neon Auth session now. */ export function buildDevAdmin(role: AdminRole): AdminUser { const isPlatform = role === "platform_admin"; const brandId = isPlatform ? null : "dev-brand"; return { id: "dev", user_id: "dev", email: null, display_name: "Demo Admin", brand_id: brandId, brand_ids: brandId ? [brandId] : [], brand_slug: isPlatform ? null : "tuxedo", role, active: true, auth_provider: "dev", ...permissionsForRole(role), must_change_password: false, }; } function buildAdminUser(input: { id: string; email: string; displayName: string | null; brandId: string; brandName: string; brandSlug: string; role: AdminRole; active: boolean; }): AdminUser { return { id: input.id, user_id: input.id, email: input.email, display_name: input.displayName, brand_id: input.brandId, brand_ids: [input.brandId], brand_slug: input.brandSlug, role: input.role, active: input.active, auth_provider: "neon_auth", ...permissionsForRole(input.role), }; } /** * Single source of truth for "what can a role do". Used by both the * dev shim and the real user lookup so the demo and the real thing * behave identically. */ export function permissionsForRole(role: AdminRole) { if (role === "platform_admin") { return { can_manage_products: true, can_manage_stops: true, can_manage_orders: true, can_manage_pickup: true, can_manage_messages: true, can_manage_refunds: true, can_manage_users: true, can_manage_water_log: true, can_manage_reports: true, can_manage_settings: true, can_manage_billing: true, can_manage_branding: true, can_manage_marketing: true, can_manage_team: true, }; } if (role === "brand_admin") { return { can_manage_products: true, can_manage_stops: true, can_manage_orders: true, can_manage_pickup: true, can_manage_messages: true, can_manage_refunds: true, can_manage_users: false, can_manage_water_log: true, can_manage_reports: true, can_manage_settings: true, can_manage_billing: true, can_manage_branding: true, can_manage_marketing: true, can_manage_team: true, }; } // store_employee return { can_manage_products: false, can_manage_stops: false, can_manage_orders: true, can_manage_pickup: true, can_manage_messages: false, can_manage_refunds: false, can_manage_users: false, can_manage_water_log: false, can_manage_reports: false, can_manage_settings: false, can_manage_billing: false, can_manage_branding: false, can_manage_marketing: false, can_manage_team: false, }; }