Files
route-commerce/src/lib/admin-permissions.ts
T
openclaw 653dce747b
Deploy to route.crispygoat.com / deploy (push) Has been cancelled
fix: select role from admin_users instead of admin_user_brands
The membership query in getAdminUser() was selecting role from
adminUserBrands.adminUserId, which is wrong - admin_user_brands has no
role column. Role is stored in admin_users. Also added try/catch
around withPlatformAdmin to prevent DB errors from throwing.
2026-06-09 14:42:43 -06:00

237 lines
7.3 KiB
TypeScript

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<AdminUser | null> {
// 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<TenantContext | null> {
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,
};
}