fix: admin auth for prod Neon Auth deployment
Deploy to route.crispygoat.com / deploy (push) Successful in 3m59s
Deploy to route.crispygoat.com / deploy (push) Successful in 3m59s
- getAdminUser now properly supports platform_admin with 0 brand links and loads full brand_ids - createAdminUser action now inserts into admin_user_brands join table - Admin layout surfaces the signed-in email on Access Denied - AdminAccessDenied links to /login instead of dead-end /admin - Main dashboard uses direct pool query instead of dead supabase shim - Improved provision-admin.ts script for prod bootstrap (loads .env.production too)
This commit is contained in:
@@ -18,13 +18,15 @@ import type { AdminRole, AdminUser, TenantContext } from "@/lib/admin-permission
|
||||
* 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)
|
||||
* - 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)
|
||||
*
|
||||
* 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.
|
||||
* 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<AdminUser | null> {
|
||||
// Check for dev_session cookie in development mode
|
||||
@@ -67,37 +69,41 @@ export async function getAdminUser(): Promise<AdminUser | null> {
|
||||
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,
|
||||
// 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);
|
||||
.where(eq(adminUserBrands.adminUserId, user.id));
|
||||
|
||||
console.log("[admin-permissions] Membership rows found:", membershipRows.length, membershipRows);
|
||||
if (membershipRows.length === 0) {
|
||||
|
||||
// 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.
|
||||
console.log("[admin-permissions] No brand memberships for non-platform user — denying");
|
||||
return null;
|
||||
}
|
||||
|
||||
const m = membershipRows[0];
|
||||
const role = user.role as AdminRole;
|
||||
const first = membershipRows[0] ?? null;
|
||||
return buildAdminUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
displayName: user.name,
|
||||
brandId: m.brandId,
|
||||
brandName: m.brandName,
|
||||
brandSlug: m.brandSlug,
|
||||
brandId: first?.brandId ?? null,
|
||||
brandName: first?.brandName ?? null,
|
||||
brandSlug: first?.brandSlug ?? null,
|
||||
role,
|
||||
active: true,
|
||||
brandIds: membershipRows.map((m) => m.brandId),
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -164,19 +170,23 @@ function buildAdminUser(input: {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string | null;
|
||||
brandId: string;
|
||||
brandName: string;
|
||||
brandSlug: string;
|
||||
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: [input.brandId],
|
||||
brand_ids: brandIds,
|
||||
brand_slug: input.brandSlug,
|
||||
role: input.role,
|
||||
active: input.active,
|
||||
|
||||
Reference in New Issue
Block a user