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 is a brand-scoped role (brand_admin / store_employee) and has no * rows in `admin_user_brands` (not provisioned for any brand yet) * * Platform admins may be provisioned with zero brand links and still receive * access (they see all brands). Brand-scoped admins require >= 1 link row. * * Provisioning: an admin must ensure rows exist in both tables for the user's * email (matched from their Neon Auth session). 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 role = (user.role as AdminRole) || "brand_admin"; // Load all brand memberships for this admin (supports multi-brand admins). // The redundant adminUsers join was removed; role lives on admin_users. const membershipRows = await db .select({ brandId: adminUserBrands.brandId, brandName: brands.name, brandSlug: brands.slug, }) .from(adminUserBrands) .innerJoin(brands, eq(brands.id, adminUserBrands.brandId)) .where(eq(adminUserBrands.adminUserId, user.id)); // Brand-scoped roles (brand_admin, store_employee) require at least one brand link. // Platform admins may have zero or more explicit links; they get cross-brand access via role. if (membershipRows.length === 0 && role !== "platform_admin") { // Signed in but not provisioned for any brand. return null; } const first = membershipRows[0] ?? null; return buildAdminUser({ id: user.id, email: user.email, displayName: user.name, brandId: first?.brandId ?? null, brandName: first?.brandName ?? null, brandSlug: first?.brandSlug ?? null, role, active: true, brandIds: membershipRows.map((m) => m.brandId), }); }); } 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 | null; brandName: string | null; brandSlug: string | null; role: AdminRole; active: boolean; brandIds?: string[]; }): AdminUser { const brandIds = input.brandIds && input.brandIds.length > 0 ? input.brandIds : (input.brandId ? [input.brandId] : []); return { id: input.id, user_id: input.id, email: input.email, display_name: input.displayName, brand_id: input.brandId, brand_ids: brandIds, 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, }; }