916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
278 lines
11 KiB
PL/PgSQL
278 lines
11 KiB
PL/PgSQL
-- Migration 070: RLS Policy Audit Fixes
|
|
-- 1. Add RLS to products (was completely missing)
|
|
-- 2. Add brand scoping to stops RLS
|
|
-- 3. Add deleted_at IS NULL to wholesale_customers and wholesale_products RLS
|
|
-- 4. Add SELECT policy to order_items (brand-scoped via orders/stops JOIN)
|
|
|
|
BEGIN;
|
|
|
|
-- ── 1. products — Add RLS ──────────────────────────────────────────────────────
|
|
-- Table has brand_id column but no RLS policies whatsoever.
|
|
-- Add SELECT for admins (brand-scoped), INSERT/UPDATE/DELETE blocked (RPC only).
|
|
|
|
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Platform admin can read all products
|
|
CREATE POLICY "platform_admin_read_products" ON public.products
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role = 'platform_admin'
|
|
)
|
|
);
|
|
|
|
-- Brand admin can read their brand's products
|
|
CREATE POLICY "brand_admin_read_products" ON public.products
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role = 'brand_admin'
|
|
AND au.brand_id = products.brand_id
|
|
)
|
|
);
|
|
|
|
-- Store employee can read their brand's products
|
|
CREATE POLICY "store_employee_read_products" ON public.products
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role = 'store_employee'
|
|
AND au.brand_id = products.brand_id
|
|
)
|
|
);
|
|
|
|
-- wholesale_customer can read their brand's products (via brand_id join)
|
|
CREATE POLICY "wholesale_customer_read_products" ON public.products
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
JOIN wholesale_customers wc ON wc.user_id = au.user_id
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role = 'wholesale_customer'
|
|
AND wc.brand_id = products.brand_id
|
|
)
|
|
);
|
|
|
|
-- Block INSERT/UPDATE/DELETE on products — all writes go through RPCs
|
|
-- No policy = no access for mutating operations (authenticated users blocked)
|
|
CREATE POLICY "block_products_mutations" ON public.products
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_products_update" ON public.products
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_products_delete" ON public.products
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 2. stops — Add brand scoping ───────────────────────────────────────────────
|
|
-- Current policy allows ANY admin to read ALL stops across all brands.
|
|
-- Replace with brand-scoped policies.
|
|
|
|
DROP POLICY IF EXISTS "Admins can read stops" ON stops;
|
|
ALTER TABLE stops DISABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE stops ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Platform admin sees all stops
|
|
CREATE POLICY "platform_admin_read_stops" ON stops
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role = 'platform_admin'
|
|
)
|
|
);
|
|
|
|
-- Brand admin / store_employee sees only their brand's stops
|
|
CREATE POLICY "brand_admin_read_stops" ON stops
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role IN ('brand_admin', 'store_employee')
|
|
AND au.brand_id = stops.brand_id
|
|
)
|
|
);
|
|
|
|
-- Block all mutations on stops (admin only via RPC)
|
|
CREATE POLICY "block_stops_mutations" ON stops
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_stops_update" ON stops
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_stops_delete" ON stops
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 3. wholesale_customers — Add deleted_at IS NULL ───────────────────────────
|
|
-- Soft-deleted customers (deleted_at NOT NULL) should not be readable via REST.
|
|
-- Split into two policies: one for platform_admin (no deleted filter), one for brand_admin.
|
|
|
|
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_customers" ON wholesale_customers;
|
|
DROP POLICY IF EXISTS "wholesale_customer_read_own" ON wholesale_customers;
|
|
ALTER TABLE wholesale_customers DISABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE wholesale_customers ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Platform admin can read all customers (including soft-deleted for recovery)
|
|
CREATE POLICY "platform_admin_manage_wholesale_customers" ON wholesale_customers
|
|
FOR ALL USING (
|
|
current_setting('app.settings.role', true)::TEXT = 'platform_admin'
|
|
);
|
|
|
|
-- Brand admin can only manage their brand's non-deleted customers
|
|
CREATE POLICY "brand_admin_manage_wholesale_customers" ON wholesale_customers
|
|
FOR ALL USING (
|
|
current_setting('app.settings.role', true)::TEXT = 'brand_admin'
|
|
AND brand_id = current_setting('app.settings.brand_id', true)::UUID
|
|
AND deleted_at IS NULL
|
|
);
|
|
|
|
-- Customers can read their own record (not brand-admin, wholesale_customer role)
|
|
CREATE POLICY "wholesale_customer_read_own" ON wholesale_customers
|
|
FOR SELECT USING (
|
|
current_setting('app.settings.role', true)::TEXT = 'wholesale_customer'
|
|
AND user_id = current_setting('app.settings.user_id', true)::UUID
|
|
AND deleted_at IS NULL
|
|
);
|
|
|
|
-- ── 4. wholesale_products — Add deleted_at IS NULL ───────────────────────────
|
|
-- Soft-deleted products should not be readable via REST.
|
|
|
|
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_products" ON wholesale_products;
|
|
ALTER TABLE wholesale_products DISABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE wholesale_products ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Platform admin can read all (including soft-deleted for recovery)
|
|
CREATE POLICY "platform_admin_manage_wholesale_products" ON wholesale_products
|
|
FOR ALL USING (
|
|
current_setting('app.settings.role', true)::TEXT = 'platform_admin'
|
|
);
|
|
|
|
-- Brand admin can only manage their brand's non-deleted products
|
|
CREATE POLICY "brand_admin_manage_wholesale_products" ON wholesale_products
|
|
FOR ALL USING (
|
|
current_setting('app.settings.role', true)::TEXT = 'brand_admin'
|
|
AND brand_id = current_setting('app.settings.brand_id', true)::UUID
|
|
AND deleted_at IS NULL
|
|
);
|
|
|
|
-- ── 5. order_items — Add brand-scoped SELECT policy ───────────────────────────
|
|
-- order_items currently has no RLS policies (all access blocked).
|
|
-- Add SELECT with brand scoping via orders/stops JOIN.
|
|
|
|
ALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Platform admin can read all order items
|
|
CREATE POLICY "platform_admin_read_order_items" ON order_items
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role = 'platform_admin'
|
|
)
|
|
);
|
|
|
|
-- Brand admin / store_employee: can read order items for their brand's orders
|
|
CREATE POLICY "brand_admin_read_order_items" ON order_items
|
|
FOR SELECT USING (
|
|
EXISTS (
|
|
SELECT 1 FROM admin_users au
|
|
JOIN orders o ON o.id = order_items.order_id
|
|
JOIN stops s ON s.id = o.stop_id
|
|
WHERE au.user_id = auth.uid()
|
|
AND au.role IN ('brand_admin', 'store_employee')
|
|
AND s.brand_id = au.brand_id
|
|
)
|
|
);
|
|
|
|
-- Block all mutations on order_items (admin only via RPC)
|
|
CREATE POLICY "block_order_items_mutations" ON order_items
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_order_items_update" ON order_items
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_order_items_delete" ON order_items
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 6. orders — Add brand scoping to write policies ───────────────────────────
|
|
-- Existing SELECT policies use stops JOIN correctly.
|
|
-- Ensure INSERT/UPDATE/DELETE policies also enforce brand scoping (or block them).
|
|
-- Note: orders INSERT/UPDATE/DELETE go through RPCs — add blocking policies as safety net.
|
|
|
|
CREATE POLICY "block_orders_insert" ON orders
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_orders_update" ON orders
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_orders_delete" ON orders
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 7. wholesale_orders — Ensure no deleted_at issues ─────────────────────────
|
|
-- wholesale_orders has no deleted_at column (hard delete). No changes needed.
|
|
-- But add explicit INSERT/UPDATE/DELETE block to match other tables.
|
|
-- All writes go through RPCs — block direct REST mutations.
|
|
|
|
CREATE POLICY "block_wholesale_orders_insert" ON wholesale_orders
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_wholesale_orders_update" ON wholesale_orders
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_wholesale_orders_delete" ON wholesale_orders
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 8. wholesale_order_items — Block direct REST mutations ────────────────────
|
|
-- SELECT is already scoped in 069. Block INSERT/UPDATE/DELETE.
|
|
|
|
CREATE POLICY "block_wholesale_order_items_insert" ON wholesale_order_items
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_wholesale_order_items_update" ON wholesale_order_items
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_wholesale_order_items_delete" ON wholesale_order_items
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 9. wholesale_deposits — Block direct REST mutations ───────────────────────
|
|
-- SELECT is already scoped in 069. Block INSERT/UPDATE/DELETE.
|
|
|
|
CREATE POLICY "block_wholesale_deposits_insert" ON wholesale_deposits
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_wholesale_deposits_update" ON wholesale_deposits
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_wholesale_deposits_delete" ON wholesale_deposits
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 10. wholesale_customers — Block direct REST mutations ──────────────────────
|
|
-- SELECT policies already set above. Block mutations.
|
|
|
|
CREATE POLICY "block_wholesale_customers_insert" ON wholesale_customers
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_wholesale_customers_update" ON wholesale_customers
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_wholesale_customers_delete" ON wholesale_customers
|
|
FOR DELETE USING (false);
|
|
|
|
-- ── 11. wholesale_products — Block direct REST mutations ───────────────────────
|
|
-- SELECT policies already set above. Block mutations.
|
|
|
|
CREATE POLICY "block_wholesale_products_insert" ON wholesale_products
|
|
FOR INSERT WITH CHECK (false);
|
|
|
|
CREATE POLICY "block_wholesale_products_update" ON wholesale_products
|
|
FOR UPDATE USING (false);
|
|
|
|
CREATE POLICY "block_wholesale_products_delete" ON wholesale_products
|
|
FOR DELETE USING (false);
|
|
|
|
COMMIT;
|
|
|
|
NOTIFY pgrst, 'reload schema'; |