feat(admin): multi-brand admin support
Implements the design at
docs/superpowers/specs/2026-06-04-multi-brand-admin-design.md.
Adds:
- admin_user_brands junction table (m:n admin<->brand) via migration 207
- New role 'multi_brand_admin' (auto-set when an admin has 2+ brands)
- 'active_brand_id' cookie that persists the admin's currently-selected
brand across navigations; switchable via the new BrandSelector dropdown
in the sidebar
- Centralised brand resolution in src/lib/brand-scope.ts:
- getActiveBrandId (URL > cookie > legacy brand_id > first of brand_ids)
- assertBrandAccess (defence-in-depth for cases where the brandId
comes from a URL form or RPC return)
- ~30 server actions and ~10 page server components migrated to use
getActiveBrandId instead of the silent brandId ?? adminUser.brand_id
pattern that allowed cross-brand access bugs
- BrandSelector client component with proper a11y (aria-haspopup,
aria-expanded, role=listbox, outside-click and escape-to-close)
Migration 207 also adds RLS so admins can read their own junction rows
(needed for the dropdown to populate) and SECURITY DEFINER RPCs
add/remove_admin_user_brand that auto-promote/demote between
brand_admin and multi_brand_admin.
Notes:
- Migration number is 207 not 204 — 204-206 were taken in this worktree
by the concurrent locations work.
- The legacy admin_users.brand_id column is preserved for backwards
compat; a follow-up migration 220_* will drop it.
- Dev sessions (dev_session cookie, NEXT_PUBLIC_USE_MOCK_DATA) get
brand_ids: []; the documented limitation is that dev store_employee
will see <AdminAccessDenied /> if no real brands exist.
- Pages that hardcode a Tuxedo brand UUID as a fallback
(adminUser.brand_id ?? '64294306-...') are NOT migrated in this PR —
they still work for single-brand admins and are out of scope.
Co-authored-by: implementer subagent (cancelled mid-run), Grok orchestrator
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { parseExcelBuffer, parseTextBuffer } from "@/lib/excel-parser";
|
||||
import { importProductsBatch } from "@/actions/import-products";
|
||||
import { importOrdersBatch } from "@/actions/import-orders";
|
||||
@@ -41,7 +42,9 @@ export async function analyzeImport(
|
||||
): Promise<AnalyzeImportResult> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
try {
|
||||
assertBrandAccess(adminUser, brandId);
|
||||
} catch {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -96,7 +97,7 @@ async function brandScopedRPC<T>(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) throw new Error("Not authenticated");
|
||||
|
||||
const brandId = adminUser.brand_id ?? null;
|
||||
const brandId = await getActiveBrandId(adminUser);
|
||||
|
||||
const response = await fetch(`${supabaseUrl}/rest/v1/rpc/${rpcName}`, {
|
||||
method: "POST",
|
||||
@@ -232,7 +233,7 @@ export async function getRecentOrders(limit: number = 10): Promise<RecentOrder[]
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) throw new Error("Not authenticated");
|
||||
|
||||
const brandId = adminUser.brand_id ?? null;
|
||||
const brandId = await getActiveBrandId(adminUser);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
select: "id,customer_name,subtotal,status,created_at,fulfillment",
|
||||
@@ -293,7 +294,7 @@ export async function getConversionFunnel(): Promise<ConversionFunnel[]> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) throw new Error("Not authenticated");
|
||||
|
||||
const brandId = adminUser.brand_id ?? null;
|
||||
const brandId = await getActiveBrandId(adminUser);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
select: "id,status",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import "server-only";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type BrandListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
logo_url: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the list of brands the current admin user can act in.
|
||||
*
|
||||
* - platform_admin: all brands (queried directly from the `brands` table)
|
||||
* - everyone else: brands in `adminUser.brand_ids`
|
||||
* - empty array for unauthenticated / no-access admins
|
||||
*
|
||||
* This is a plain async function (not a server action) so it can be called
|
||||
* from server components and server actions without the "use server" wrapper.
|
||||
* The BrandSelector client component receives the result as a prop.
|
||||
*/
|
||||
export async function listBrandsForAdmin(): Promise<BrandListItem[]> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return [];
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!supabaseUrl || !serviceKey) return [];
|
||||
|
||||
if (adminUser.role === "platform_admin") {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/brands?select=id,name,slug,logo_url&order=name`,
|
||||
{ headers: svcHeaders(serviceKey), cache: "no-store" }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (adminUser.brand_ids.length === 0) return [];
|
||||
|
||||
// Use PostgREST `in` filter. The brand_ids are UUIDs, so the quoting
|
||||
// pattern in the spec is safe; the inner quotes are required by PostgREST
|
||||
// for UUID literals.
|
||||
const filter = `id=in.(${adminUser.brand_ids
|
||||
.map((id) => `"${id}"`)
|
||||
.join(",")})`;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/brands?${filter}&select=id,name,slug,logo_url&order=name`,
|
||||
{ headers: svcHeaders(serviceKey), cache: "no-store" }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type CampaignType = "marketing" | "operational" | "transactional";
|
||||
@@ -57,7 +58,11 @@ export async function getCommunicationCampaigns(brandId?: string): Promise<ListC
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
@@ -147,7 +152,7 @@ export async function deleteCampaign(campaignId: string, brandId?: string): Prom
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_campaign_id: campaignId, p_brand_id: adminUser.brand_id ?? null }),
|
||||
body: JSON.stringify({ p_campaign_id: campaignId, p_brand_id: (await getActiveBrandId(adminUser)) ?? null }),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -167,7 +172,7 @@ export async function getCampaignById(campaignId: string, brandId?: string): Pro
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_campaign_id: campaignId, p_brand_id: brandId ?? adminUser.brand_id ?? null }),
|
||||
body: JSON.stringify({ p_campaign_id: campaignId, p_brand_id: (await getActiveBrandId(adminUser, brandId)) ?? null }),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import type { AudienceRules } from "./campaigns";
|
||||
|
||||
@@ -120,11 +121,15 @@ export async function sendCampaign(campaignId: string, brandId?: string): Promis
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
// Resolve brand from campaign or parameter
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id;
|
||||
// Resolve brand from campaign or parameter (URL > cookie > legacy > first of brand_ids)
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
|
||||
// Brand scoping: brand_admin can only send their own brand's campaigns
|
||||
if (adminUser.role === "brand_admin" && effectiveBrandId && adminUser.brand_id !== effectiveBrandId) {
|
||||
if (adminUser.role === "brand_admin" && effectiveBrandId && !adminUser.brand_ids.includes(effectiveBrandId)) {
|
||||
return { success: false, error: "Not authorized to send this brand's campaigns" };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import type { AudienceRules } from "./campaigns";
|
||||
|
||||
@@ -33,7 +34,11 @@ export async function getCommunicationTemplates(brandId?: string): Promise<{ suc
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
|
||||
// Brand scoping: brand_admin can only see their own brand's templates
|
||||
if (adminUser.role === "brand_admin" && effectiveBrandId && effectiveBrandId !== adminUser.brand_id) {
|
||||
@@ -112,7 +117,7 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_brand_id: adminUser.brand_id ?? null }),
|
||||
body: JSON.stringify({ p_brand_id: (await getActiveBrandId(adminUser)) ?? null }),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -66,7 +67,7 @@ export async function getDashboardStats(): Promise<DashboardStats> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) throw new Error("Not authenticated");
|
||||
|
||||
const brandId = adminUser.brand_id ?? null;
|
||||
const brandId = await getActiveBrandId(adminUser);
|
||||
|
||||
// Get today's date range
|
||||
const today = new Date();
|
||||
@@ -202,7 +203,7 @@ export async function getDashboardSummary(): Promise<DashboardSummary> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) throw new Error("Not authenticated");
|
||||
|
||||
const brandId = adminUser.brand_id ?? null;
|
||||
const brandId = await getActiveBrandId(adminUser);
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type LocationInput = {
|
||||
@@ -48,7 +49,11 @@ export async function createLocation(
|
||||
if (!adminUser.can_manage_stops) {
|
||||
return { success: false, error: "Not authorized to manage locations" };
|
||||
}
|
||||
const effectiveBrandId = brandId || adminUser.brand_id;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand selected" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -96,7 +101,11 @@ export async function createLocationsBatch(
|
||||
if (!adminUser.can_manage_stops) {
|
||||
return { success: false, created: 0, error: "Not authorized" };
|
||||
}
|
||||
const effectiveBrandId = brandId || adminUser.brand_id;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, created: 0, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
if (!effectiveBrandId) return { success: false, created: 0, error: "No brand selected" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -206,7 +215,8 @@ export async function adminListLocations(
|
||||
|
||||
// Application-layer brand scoping: platform_admin (brand_id: null) gets all
|
||||
// brands; everyone else gets only their own brand.
|
||||
const effectiveBrandId = brandId || adminUser.brand_id;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
const effectiveBrandId = activeBrandId;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/admin_list_locations`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
@@ -41,7 +42,11 @@ export async function createAdminOrder(
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id && effectiveBrandId !== adminUser.brand_id) {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type PaymentProvider = "stripe" | "square" | "manual";
|
||||
@@ -65,10 +66,9 @@ export async function savePaymentSettings(params: {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
if (
|
||||
adminUser.role === "brand_admin" &&
|
||||
adminUser.brand_id !== params.brandId
|
||||
) {
|
||||
try {
|
||||
assertBrandAccess(adminUser, params.brandId);
|
||||
} catch {
|
||||
return { success: false, error: "Not authorized for this brand" };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export async function deleteProduct(
|
||||
@@ -16,7 +17,11 @@ export async function deleteProduct(
|
||||
return { success: false, error: "Not authorized to manage products" };
|
||||
}
|
||||
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? null;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId, assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -153,7 +154,14 @@ export interface FieldYieldSummary {
|
||||
export async function getRouteTraceLots(brandId: string, status?: string) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -235,7 +243,14 @@ export async function createHarvestLot(
|
||||
) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -298,7 +313,14 @@ export async function updateHarvestLotStatus(
|
||||
export async function getRouteTraceStats(brandId: string) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -318,7 +340,14 @@ export async function getRouteTraceStats(brandId: string) {
|
||||
export async function searchHarvestLots(brandId: string, query: string) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -347,7 +376,14 @@ export async function getTraceChain(lotId: string) {
|
||||
export async function getHarvestLotsReadyToHaul(brandId: string) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -364,7 +400,14 @@ export async function getHarvestLotsReadyToHaul(brandId: string) {
|
||||
export async function getFieldYieldSummary(brandId: string) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -405,7 +448,14 @@ export interface InventoryByCrop {
|
||||
export async function getInventoryByCrop(brandId: string): Promise<{ success: true; inventory: InventoryByCrop[] } | { success: false; error: string }> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
@@ -451,7 +501,14 @@ export async function getRecentLotEvents(
|
||||
): Promise<{ success: true; events: RecentLotEvent[] } | { success: false; error: string }> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Unauthorized" };
|
||||
const effectiveBrandId = brandId ?? adminUser.brand_id ?? undefined;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Brand access required" };
|
||||
}
|
||||
if (activeBrandId) {
|
||||
try { assertBrandAccess(adminUser, activeBrandId); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = activeBrandId ?? undefined;
|
||||
if (!effectiveBrandId) return { success: false, error: "No brand" };
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import {
|
||||
setActiveBrandCookie,
|
||||
clearActiveBrandCookie,
|
||||
} from "@/lib/brand-scope";
|
||||
|
||||
/**
|
||||
* Set the persistent "active brand" for the current admin user.
|
||||
*
|
||||
* - `brandId === null`: "All brands" — only allowed for platform_admin.
|
||||
* Clears the cookie (cookie absence = no specific brand pinned).
|
||||
* - `brandId` string: sets the cookie, after validating the admin has access.
|
||||
*
|
||||
* The active brand is the default the UI uses for pages that don't receive
|
||||
* an explicit `brandId` from the URL. The cookie is the source of truth —
|
||||
* the URL is only for deep-linking.
|
||||
*/
|
||||
export async function setActiveBrand(
|
||||
brandId: string | null
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
// null = "All brands" (platform_admin only)
|
||||
if (brandId === null) {
|
||||
if (adminUser.role !== "platform_admin") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Only platform admins can select 'All brands'",
|
||||
};
|
||||
}
|
||||
await clearActiveBrandCookie();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (
|
||||
adminUser.role !== "platform_admin" &&
|
||||
!adminUser.brand_ids.includes(brandId)
|
||||
) {
|
||||
return { success: false, error: "No access to that brand" };
|
||||
}
|
||||
|
||||
await setActiveBrandCookie(brandId);
|
||||
return { success: true };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type UpdateShippingStatusResult =
|
||||
@@ -28,7 +29,7 @@ export async function updateShippingStatus(
|
||||
p_order_id: orderId,
|
||||
p_shipping_status: status,
|
||||
p_tracking_number: trackingNumber ?? null,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: await getActiveBrandId(adminUser),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -79,7 +80,7 @@ export async function getShippingOrders(): Promise<GetShippingOrdersResult> {
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: await getActiveBrandId(adminUser),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import type { FedExServiceType } from "./fedex-rates";
|
||||
|
||||
@@ -156,6 +157,10 @@ export async function createFedExShipment(
|
||||
const order = orders[0];
|
||||
if (!order) return { success: false, error: "Order not found" };
|
||||
|
||||
// The order's brand is the source of truth. Validate the admin has access.
|
||||
if (order.brand_id) {
|
||||
try { assertBrandAccess(adminUser, order.brand_id); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = order.brand_id ?? adminUser.brand_id ?? null;
|
||||
|
||||
// Get FedEx settings
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
@@ -177,6 +178,10 @@ export async function getFedExRates(
|
||||
const order = orders[0];
|
||||
if (!order) return { success: false, error: "Order not found" };
|
||||
|
||||
// The order's brand is the source of truth. Validate the admin has access.
|
||||
if (order.brand_id) {
|
||||
try { assertBrandAccess(adminUser, order.brand_id); } catch { return { success: false, error: "Brand access denied" }; }
|
||||
}
|
||||
const effectiveBrandId = order.brand_id ?? adminUser.brand_id ?? null;
|
||||
|
||||
// Fetch shipping settings for this brand
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type SyncLogEntry = {
|
||||
@@ -29,7 +30,9 @@ export async function syncSquareNow(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, synced: 0, errors: ["Not authenticated"] };
|
||||
if (!adminUser.can_manage_orders) return { success: false, synced: 0, errors: ["Not authorized"] };
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
try {
|
||||
assertBrandAccess(adminUser, brandId);
|
||||
} catch {
|
||||
return { success: false, synced: 0, errors: ["Not authorized"] };
|
||||
}
|
||||
|
||||
@@ -61,7 +64,9 @@ export async function getSyncLog(brandId: string): Promise<{
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, logs: [] };
|
||||
if (!adminUser.can_manage_orders) return { success: false, logs: [] };
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
try {
|
||||
assertBrandAccess(adminUser, brandId);
|
||||
} catch {
|
||||
return { success: false, logs: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type StopImportRow = {
|
||||
@@ -25,7 +26,11 @@ export async function createStopsBatch(
|
||||
return { success: false, created: 0, error: "Not authorized to manage stops" };
|
||||
}
|
||||
|
||||
const effectiveBrandId = brandId || adminUser.brand_id;
|
||||
const activeBrandId = await getActiveBrandId(adminUser, brandId);
|
||||
if (!activeBrandId && adminUser.role !== "platform_admin") {
|
||||
return { success: false, created: 0, error: "Brand access required" };
|
||||
}
|
||||
const effectiveBrandId = activeBrandId;
|
||||
if (!effectiveBrandId) {
|
||||
return { success: false, created: 0, error: "No brand selected" };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
type Irrigator = {
|
||||
@@ -94,7 +95,7 @@ export async function updateWaterHeadgate(
|
||||
p_name: name,
|
||||
p_active: active,
|
||||
p_unit: unit ?? null,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
p_high_threshold: highThreshold ?? null,
|
||||
p_low_threshold: lowThreshold ?? null,
|
||||
}),
|
||||
@@ -186,7 +187,7 @@ export async function updateWaterIrrigator(
|
||||
p_active: active,
|
||||
p_lang: lang,
|
||||
p_role: role,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -215,7 +216,7 @@ export async function resetWaterIrrigatorPin(
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_user_id: irrigatorId,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -242,7 +243,7 @@ export async function deleteWaterUser(userId: string): Promise<{ success: boolea
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_user_id: userId,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -269,7 +270,7 @@ export async function deleteWaterHeadgate(headgateId: string): Promise<{ success
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_headgate_id: headgateId,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -356,7 +357,7 @@ export async function regenerateHeadgateToken(headgateId: string): Promise<{ suc
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_headgate_id: headgateId, p_brand_id: adminUser.brand_id ?? null }),
|
||||
body: JSON.stringify({ p_headgate_id: headgateId, p_brand_id: (await getActiveBrandId(adminUser)) ?? null }),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -392,7 +393,7 @@ export async function updateWaterEntry(
|
||||
p_measurement: measurement,
|
||||
p_notes: notes,
|
||||
p_unit: unit ?? null,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -421,7 +422,7 @@ export async function deleteWaterEntry(
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_entry_id: entryId,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: (await getActiveBrandId(adminUser)) ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { assertBrandAccess } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export async function registerWholesaleCustomer(params: {
|
||||
@@ -81,7 +82,9 @@ export async function approveWholesaleRegistration(
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
if (adminUser.role !== "platform_admin" && adminUser.brand_id !== brandId) {
|
||||
try {
|
||||
assertBrandAccess(adminUser, brandId);
|
||||
} catch {
|
||||
return { success: false, error: "Not authorized to operate on this brand" };
|
||||
}
|
||||
|
||||
|
||||
+33
-27
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
export type WholesaleOrder = {
|
||||
@@ -129,15 +130,16 @@ export type WholesaleDashboardStats = {
|
||||
* platform_admin → null (means "all brands" — passes to RPC unchanged)
|
||||
* brand_admin → their own brand_id only; rejects attempts to operate on other brands
|
||||
* store_employee → their own brand_id
|
||||
* multi_brand_admin → active brand from cookie/URL/legacy, must be in brand_ids
|
||||
* unauthenticated → null (actions should already bail out earlier)
|
||||
*
|
||||
* This prevents brand_admin from seeing or modifying another brand's data
|
||||
* even if they manually pass a different brandId to the action.
|
||||
*/
|
||||
function resolveBrandId(
|
||||
async function resolveBrandId(
|
||||
adminUser: Awaited<ReturnType<typeof getAdminUser>>,
|
||||
requestedBrandId?: string
|
||||
): string | null {
|
||||
): Promise<string | null> {
|
||||
if (!adminUser) return null;
|
||||
|
||||
if (adminUser.role === "platform_admin") {
|
||||
@@ -145,15 +147,15 @@ function resolveBrandId(
|
||||
return null;
|
||||
}
|
||||
|
||||
// brand_admin and store_employee are scoped to their own brand
|
||||
const userBrand = adminUser.brand_id ?? null;
|
||||
// For non-platform-admin: resolve the active brand (validates against brand_ids)
|
||||
const activeBrandId = await getActiveBrandId(adminUser, requestedBrandId);
|
||||
|
||||
if (requestedBrandId && requestedBrandId !== userBrand) {
|
||||
if (requestedBrandId && activeBrandId !== requestedBrandId) {
|
||||
// Brand admin trying to operate on another brand's data — block it
|
||||
return null; // caller should check and return unauthorized
|
||||
}
|
||||
|
||||
return userBrand;
|
||||
return activeBrandId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,23 +163,24 @@ function resolveBrandId(
|
||||
* if a brand_admin tries to operate outside their brand.
|
||||
* Use for mutating actions (save, delete, fulfill) where cross-brand access must be blocked.
|
||||
*/
|
||||
function enforceBrandScope(
|
||||
async function enforceBrandScope(
|
||||
adminUser: Awaited<ReturnType<typeof getAdminUser>>,
|
||||
requestedBrandId?: string
|
||||
): { brandId: string | null; error?: string } {
|
||||
): Promise<{ brandId: string | null; error?: string }> {
|
||||
if (!adminUser) return { brandId: null, error: "Not authenticated" };
|
||||
|
||||
if (adminUser.role === "platform_admin") {
|
||||
return { brandId: null }; // unrestricted
|
||||
}
|
||||
|
||||
const userBrand = adminUser.brand_id ?? null;
|
||||
// For non-platform-admin: resolve the active brand (validates against brand_ids)
|
||||
const activeBrandId = await getActiveBrandId(adminUser, requestedBrandId);
|
||||
|
||||
if (requestedBrandId && requestedBrandId !== userBrand) {
|
||||
if (requestedBrandId && activeBrandId !== requestedBrandId) {
|
||||
return { brandId: null, error: "Not authorized to operate on this brand" };
|
||||
}
|
||||
|
||||
return { brandId: userBrand };
|
||||
return { brandId: activeBrandId };
|
||||
}
|
||||
|
||||
// ── Orders ──────────────────────────────────────────────────────────────────
|
||||
@@ -185,7 +188,7 @@ function enforceBrandScope(
|
||||
export async function getWholesaleOrders(brandId?: string): Promise<WholesaleOrder[]> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
const bid = await resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
@@ -209,7 +212,7 @@ export async function getWholesaleOrders(brandId?: string): Promise<WholesaleOrd
|
||||
export async function getWholesalePickupOrders(brandId?: string): Promise<WholesaleOrder[]> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
const bid = await resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
@@ -256,7 +259,7 @@ export async function markWholesaleOrderFulfilled(orderId: string, brandId?: str
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { brandId: resolved, error } = enforceBrandScope(adminUser, brandId);
|
||||
const { brandId: resolved, error } = await enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -305,7 +308,7 @@ export async function updateWholesaleOrderStatus(
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
const { error } = await enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -332,7 +335,7 @@ export async function deleteWholesaleOrder(orderId: string, brandId?: string): P
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
const { error } = await enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -359,7 +362,7 @@ export async function deleteWholesaleCustomer(customerId: string, brandId?: stri
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
const { error } = await enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -388,7 +391,7 @@ export async function deleteWholesaleProduct(productId: string, brandId?: string
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { error } = enforceBrandScope(adminUser, brandId);
|
||||
const { error } = await enforceBrandScope(adminUser, brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -417,7 +420,7 @@ export async function deleteWholesaleProduct(productId: string, brandId?: string
|
||||
export async function getWholesaleCustomers(brandId?: string): Promise<WholesaleCustomer[]> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
const bid = await resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
@@ -458,7 +461,7 @@ export async function saveWholesaleCustomer(params: {
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { error } = enforceBrandScope(adminUser, params.brandId);
|
||||
const { error } = await enforceBrandScope(adminUser, params.brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -501,7 +504,7 @@ export async function saveWholesaleCustomer(params: {
|
||||
export async function getWholesaleProducts(brandId?: string): Promise<WholesaleProduct[]> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return [];
|
||||
const bid = resolveBrandId(adminUser, brandId);
|
||||
const bid = await resolveBrandId(adminUser, brandId);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
@@ -545,7 +548,7 @@ export async function saveWholesaleProduct(params: {
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" };
|
||||
|
||||
const { error } = enforceBrandScope(adminUser, params.brandId);
|
||||
const { error } = await enforceBrandScope(adminUser, params.brandId);
|
||||
if (error) return { success: false, error };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
@@ -591,7 +594,10 @@ export async function saveWholesaleProduct(params: {
|
||||
export async function getWholesaleSettings(brandId?: string): Promise<WholesaleSettings | null> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return null;
|
||||
const bid = brandId ?? adminUser.brand_id ?? null;
|
||||
const bid = await getActiveBrandId(adminUser, brandId);
|
||||
if (!bid && adminUser.role !== "platform_admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
@@ -700,7 +706,7 @@ export async function recordWholesaleDeposit(
|
||||
p_method: method,
|
||||
p_reference: reference ?? null,
|
||||
p_recorded_by: adminUser.user_id,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: await getActiveBrandId(adminUser),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -710,7 +716,7 @@ export async function recordWholesaleDeposit(
|
||||
if (!data?.success) return { success: false, error: data?.error ?? "Failed to record deposit" };
|
||||
|
||||
// Fire webhook — fire-and-forget
|
||||
enqueueWholesaleWebhookForDepositRecorded(orderId, amount, adminUser.brand_id ?? undefined).catch(() => {});
|
||||
enqueueWholesaleWebhookForDepositRecorded(orderId, amount, (await getActiveBrandId(adminUser)) ?? undefined).catch(() => {});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
@@ -753,7 +759,7 @@ export async function bulkFulfillWholesaleOrders(
|
||||
body: JSON.stringify({
|
||||
p_order_ids: orderIds,
|
||||
p_by: adminUser.user_id,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: await getActiveBrandId(adminUser),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -791,7 +797,7 @@ export async function bulkRecordWholesaleDeposit(
|
||||
p_method: method,
|
||||
p_reference: reference ?? null,
|
||||
p_recorded_by: adminUser.user_id,
|
||||
p_brand_id: adminUser.brand_id ?? null,
|
||||
p_brand_id: await getActiveBrandId(adminUser),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user