d0a12d33da
Deploy to route.crispygoat.com / deploy (push) Failing after 3m19s
Two related bugs caused the 'New Template' modal on
/admin/communications to fail with a database error:
1. The `email_templates` table was defined in `db/schema/marketing.ts`
(Drizzle) but never migrated, so the modal's `upsertTemplate`
server action errored with 'relation "email_templates" does not
exist'. Adds migration 0092 to create the table (and `campaigns`)
to match the Drizzle schema. Uses CREATE TABLE IF NOT EXISTS,
gen_random_uuid() PKs, and the standard set_updated_at trigger
pattern from 0001_init.sql.
2. `/admin/communications/page.tsx` resolved `effectiveBrandId`
with a hard-coded UUID ('64294306-...') for sessions without a
brand (e.g., dev platform_admin). That UUID isn't in the local
dev DB (brands are '11111111-...' and '22222222-...'), so the
modal would FK-violate on insert with 'violates foreign key
constraint email_templates_brand_id_fkey'. Replaced with a
`listBrandsForAdmin()` lookup that picks the first accessible
brand.
3. `listBrandsForAdmin()` (`src/actions/brands.ts`) was SELECTing
`logo_url` from `brands`, but that column doesn't exist (the
brands table only has id/name/slug/plan_tier/limits/stripe_*/...).
The query threw 'column "logo_url" does not exist', got caught
by the silent try/catch, returned [], which made #2 above render
the page with brandId="" — and any submit would then error with
'invalid input syntax for type uuid: ""'. Removed logo_url
from the SELECT; BrandListItem.logo_url is always null until a
future migration adds the column.
E2E verified: opening /admin/communications → Templates tab → New
Template → fill name → Create Template inserts a row in
email_templates with the correct brand_id.
45 lines
1.2 KiB
TypeScript
45 lines
1.2 KiB
TypeScript
import "server-only";
|
|
|
|
import { getAdminUser } from "@/lib/admin-permissions";
|
|
import { pool } from "@/lib/db";
|
|
|
|
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
|
|
*/
|
|
export async function listBrandsForAdmin(): Promise<BrandListItem[]> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return [];
|
|
|
|
try {
|
|
if (adminUser.role === "platform_admin") {
|
|
const { rows } = await pool.query<BrandListItem>(
|
|
`SELECT id, name, slug FROM brands ORDER BY name`,
|
|
);
|
|
return rows.map((r) => ({ ...r, logo_url: null }));
|
|
}
|
|
|
|
if (adminUser.brand_ids.length === 0) return [];
|
|
|
|
const { rows } = await pool.query<BrandListItem>(
|
|
`SELECT id, name, slug FROM brands
|
|
WHERE id = ANY($1::uuid[])
|
|
ORDER BY name`,
|
|
[adminUser.brand_ids],
|
|
);
|
|
return rows.map((r) => ({ ...r, logo_url: null }));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|