2026-02-06 10:33:25 +01:00
|
|
|
-- Monaco USA Portal - Post-Deployment Database Fixes
|
|
|
|
|
-- This script is IDEMPOTENT - safe to run multiple times.
|
|
|
|
|
-- Run after `docker compose up` once all containers are healthy.
|
|
|
|
|
--
|
|
|
|
|
-- Handles:
|
|
|
|
|
-- 1. Storage RLS policies (storage-api creates tables AFTER db init)
|
|
|
|
|
-- 2. Service role access grants
|
|
|
|
|
-- 3. Incremental migrations for existing databases
|
|
|
|
|
-- 4. Notifications table (if missing)
|
|
|
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
|
|
-- ============================================
|
|
|
|
|
-- 1. STORAGE POLICIES
|
|
|
|
|
-- storage-api creates storage.objects and storage.buckets with RLS enabled
|
|
|
|
|
-- but no policies. We need to add service_role policies so the portal
|
|
|
|
|
-- can upload/delete files via supabaseAdmin.
|
|
|
|
|
-- ============================================
|
|
|
|
|
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
-- Ensure service_role has BYPASSRLS
|
|
|
|
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role' AND NOT rolbypassrls) THEN
|
|
|
|
|
ALTER ROLE service_role BYPASSRLS;
|
|
|
|
|
END IF;
|
|
|
|
|
EXCEPTION
|
|
|
|
|
WHEN insufficient_privilege THEN
|
|
|
|
|
RAISE NOTICE 'Could not grant BYPASSRLS to service_role - will rely on explicit policies';
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- storage.objects policies
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'storage' AND table_name = 'objects') THEN
|
|
|
|
|
-- Drop and recreate to ensure clean state
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_select" ON storage.objects;
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_insert" ON storage.objects;
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_update" ON storage.objects;
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_delete" ON storage.objects;
|
|
|
|
|
|
|
|
|
|
CREATE POLICY "service_role_all_select" ON storage.objects
|
|
|
|
|
FOR SELECT TO service_role USING (true);
|
|
|
|
|
CREATE POLICY "service_role_all_insert" ON storage.objects
|
|
|
|
|
FOR INSERT TO service_role WITH CHECK (true);
|
|
|
|
|
CREATE POLICY "service_role_all_update" ON storage.objects
|
|
|
|
|
FOR UPDATE TO service_role USING (true);
|
|
|
|
|
CREATE POLICY "service_role_all_delete" ON storage.objects
|
|
|
|
|
FOR DELETE TO service_role USING (true);
|
|
|
|
|
|
|
|
|
|
-- Public read access for avatars and event images
|
|
|
|
|
DROP POLICY IF EXISTS "public_read_avatars" ON storage.objects;
|
|
|
|
|
CREATE POLICY "public_read_avatars" ON storage.objects
|
|
|
|
|
FOR SELECT USING (bucket_id IN ('avatars', 'event-images'));
|
|
|
|
|
|
|
|
|
|
-- Authenticated users can read documents
|
|
|
|
|
DROP POLICY IF EXISTS "authenticated_read_documents" ON storage.objects;
|
|
|
|
|
CREATE POLICY "authenticated_read_documents" ON storage.objects
|
|
|
|
|
FOR SELECT TO authenticated USING (bucket_id = 'documents');
|
|
|
|
|
|
|
|
|
|
GRANT ALL ON storage.objects TO service_role;
|
|
|
|
|
|
|
|
|
|
RAISE NOTICE 'storage.objects policies applied';
|
|
|
|
|
ELSE
|
|
|
|
|
RAISE NOTICE 'storage.objects table not found - storage-api may not have started yet';
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- storage.buckets policies
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'storage' AND table_name = 'buckets') THEN
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_buckets_select" ON storage.buckets;
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_buckets_insert" ON storage.buckets;
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_buckets_update" ON storage.buckets;
|
|
|
|
|
DROP POLICY IF EXISTS "service_role_all_buckets_delete" ON storage.buckets;
|
|
|
|
|
|
|
|
|
|
CREATE POLICY "service_role_all_buckets_select" ON storage.buckets
|
|
|
|
|
FOR SELECT TO service_role USING (true);
|
|
|
|
|
CREATE POLICY "service_role_all_buckets_insert" ON storage.buckets
|
|
|
|
|
FOR INSERT TO service_role WITH CHECK (true);
|
|
|
|
|
CREATE POLICY "service_role_all_buckets_update" ON storage.buckets
|
|
|
|
|
FOR UPDATE TO service_role USING (true);
|
|
|
|
|
CREATE POLICY "service_role_all_buckets_delete" ON storage.buckets
|
|
|
|
|
FOR DELETE TO service_role USING (true);
|
|
|
|
|
|
|
|
|
|
-- Allow authenticated users to read bucket info (needed for uploads)
|
|
|
|
|
DROP POLICY IF EXISTS "authenticated_read_buckets" ON storage.buckets;
|
|
|
|
|
CREATE POLICY "authenticated_read_buckets" ON storage.buckets
|
|
|
|
|
FOR SELECT TO authenticated USING (true);
|
|
|
|
|
|
|
|
|
|
GRANT ALL ON storage.buckets TO service_role;
|
|
|
|
|
|
|
|
|
|
RAISE NOTICE 'storage.buckets policies applied';
|
|
|
|
|
ELSE
|
|
|
|
|
RAISE NOTICE 'storage.buckets table not found - storage-api may not have started yet';
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- Ensure schema and general grants
|
|
|
|
|
GRANT USAGE ON SCHEMA storage TO service_role;
|
|
|
|
|
GRANT USAGE ON SCHEMA storage TO authenticated;
|
|
|
|
|
|
|
|
|
|
-- ============================================
|
|
|
|
|
-- 2. STORAGE BUCKETS
|
|
|
|
|
-- Ensure required buckets exist
|
|
|
|
|
-- ============================================
|
|
|
|
|
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'storage' AND table_name = 'buckets') THEN
|
|
|
|
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
|
|
|
VALUES (
|
|
|
|
|
'avatars', 'avatars', true, 5242880,
|
|
|
|
|
ARRAY['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
|
|
|
|
) ON CONFLICT (id) DO UPDATE SET public = true, file_size_limit = EXCLUDED.file_size_limit, allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
|
|
|
|
|
|
|
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
|
|
|
VALUES (
|
|
|
|
|
'documents', 'documents', true, 52428800,
|
|
|
|
|
ARRAY['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'text/plain', 'text/csv', 'application/json', 'image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
|
|
|
|
) ON CONFLICT (id) DO UPDATE SET public = true, file_size_limit = EXCLUDED.file_size_limit, allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
|
|
|
|
|
|
|
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
|
|
|
VALUES (
|
|
|
|
|
'event-images', 'event-images', true, 10485760,
|
|
|
|
|
ARRAY['image/jpeg', 'image/png', 'image/webp']
|
|
|
|
|
) ON CONFLICT (id) DO UPDATE SET public = true, file_size_limit = EXCLUDED.file_size_limit, allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
|
|
|
|
|
|
|
|
RAISE NOTICE 'Storage buckets ensured';
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- ============================================
|
|
|
|
|
-- 3. NOTIFICATIONS TABLE (added post-migration-016)
|
|
|
|
|
-- ============================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS public.notifications (
|
|
|
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
|
|
|
|
type TEXT NOT NULL CHECK (type IN ('welcome', 'event', 'payment', 'membership', 'system', 'announcement')),
|
|
|
|
|
title TEXT NOT NULL,
|
|
|
|
|
message TEXT NOT NULL,
|
|
|
|
|
link TEXT,
|
|
|
|
|
read_at TIMESTAMPTZ,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_member ON public.notifications(member_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_unread ON public.notifications(member_id) WHERE read_at IS NULL;
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_created ON public.notifications(created_at DESC);
|
|
|
|
|
|
|
|
|
|
ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY;
|
|
|
|
|
|
|
|
|
|
-- Idempotent policy creation
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.notifications'::regclass AND polname = 'Members can view own notifications') THEN
|
|
|
|
|
CREATE POLICY "Members can view own notifications"
|
|
|
|
|
ON public.notifications FOR SELECT TO authenticated
|
|
|
|
|
USING (member_id = auth.uid());
|
|
|
|
|
END IF;
|
|
|
|
|
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.notifications'::regclass AND polname = 'Members can update own notifications') THEN
|
|
|
|
|
CREATE POLICY "Members can update own notifications"
|
|
|
|
|
ON public.notifications FOR UPDATE TO authenticated
|
|
|
|
|
USING (member_id = auth.uid());
|
|
|
|
|
END IF;
|
|
|
|
|
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.notifications'::regclass AND polname = 'Admin can manage all notifications') THEN
|
|
|
|
|
CREATE POLICY "Admin can manage all notifications"
|
|
|
|
|
ON public.notifications FOR ALL TO authenticated
|
|
|
|
|
USING (EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'))
|
|
|
|
|
WITH CHECK (EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'));
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
GRANT SELECT, UPDATE ON public.notifications TO authenticated;
|
|
|
|
|
GRANT ALL ON public.notifications TO service_role;
|
|
|
|
|
|
|
|
|
|
-- Welcome notification trigger
|
|
|
|
|
CREATE OR REPLACE FUNCTION create_welcome_notification()
|
|
|
|
|
RETURNS TRIGGER AS $$
|
|
|
|
|
BEGIN
|
|
|
|
|
INSERT INTO public.notifications (member_id, type, title, message, link)
|
|
|
|
|
VALUES (
|
|
|
|
|
NEW.id, 'welcome',
|
|
|
|
|
'Welcome to Monaco USA!',
|
|
|
|
|
'Thank you for joining our community. Complete your profile and explore upcoming events.',
|
|
|
|
|
'/profile'
|
|
|
|
|
);
|
|
|
|
|
RETURN NEW;
|
|
|
|
|
END;
|
|
|
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
|
|
|
|
|
|
DROP TRIGGER IF EXISTS on_member_created_notification ON public.members;
|
|
|
|
|
CREATE TRIGGER on_member_created_notification
|
|
|
|
|
AFTER INSERT ON public.members
|
|
|
|
|
FOR EACH ROW
|
|
|
|
|
EXECUTE FUNCTION create_welcome_notification();
|
|
|
|
|
|
|
|
|
|
-- ============================================
|
2026-02-10 18:11:02 +01:00
|
|
|
-- 4. MIGRATIONS 017-025 (idempotent)
|
2026-02-06 10:33:25 +01:00
|
|
|
-- ============================================
|
|
|
|
|
|
2026-02-10 18:11:02 +01:00
|
|
|
-- --- Migration 017: Fix RLS role escalation ---
|
|
|
|
|
-- (Superseded by 019, kept for reference only)
|
|
|
|
|
|
|
|
|
|
-- --- Migration 018: Atomic Member ID Generation ---
|
|
|
|
|
DO $$
|
|
|
|
|
DECLARE
|
|
|
|
|
max_num INTEGER;
|
|
|
|
|
BEGIN
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_sequences WHERE schemaname = 'public' AND sequencename = 'member_id_seq') THEN
|
|
|
|
|
SELECT COALESCE(
|
|
|
|
|
MAX(
|
|
|
|
|
CAST(
|
|
|
|
|
SUBSTRING(member_id FROM '[0-9]+$') AS INTEGER
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
0
|
|
|
|
|
) INTO max_num
|
|
|
|
|
FROM public.members;
|
|
|
|
|
|
|
|
|
|
EXECUTE format('CREATE SEQUENCE member_id_seq START WITH %s', max_num + 1);
|
|
|
|
|
RAISE NOTICE 'Created member_id_seq starting at %', max_num + 1;
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION generate_member_id()
|
|
|
|
|
RETURNS TRIGGER AS $$
|
|
|
|
|
DECLARE
|
|
|
|
|
next_num INTEGER;
|
|
|
|
|
current_year TEXT;
|
|
|
|
|
BEGIN
|
|
|
|
|
IF NEW.member_id IS NOT NULL THEN
|
|
|
|
|
RETURN NEW;
|
|
|
|
|
END IF;
|
|
|
|
|
next_num := NEXTVAL('member_id_seq');
|
|
|
|
|
current_year := EXTRACT(YEAR FROM CURRENT_DATE)::TEXT;
|
|
|
|
|
NEW.member_id := 'MUSA-' || current_year || '-' || LPAD(next_num::TEXT, 4, '0');
|
|
|
|
|
RETURN NEW;
|
|
|
|
|
END;
|
|
|
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
|
|
|
|
DROP TRIGGER IF EXISTS set_member_id ON public.members;
|
|
|
|
|
CREATE TRIGGER set_member_id
|
|
|
|
|
BEFORE INSERT ON public.members
|
|
|
|
|
FOR EACH ROW
|
|
|
|
|
WHEN (NEW.member_id IS NULL)
|
|
|
|
|
EXECUTE FUNCTION generate_member_id();
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_members_member_id ON public.members(member_id);
|
|
|
|
|
|
|
|
|
|
-- --- Migration 019: Fix admin role assignment ---
|
2026-02-06 10:33:25 +01:00
|
|
|
DROP POLICY IF EXISTS "Users can update own profile" ON public.members;
|
2026-02-10 18:11:02 +01:00
|
|
|
DROP POLICY IF EXISTS "Members can update own non-role fields" ON public.members;
|
|
|
|
|
DROP POLICY IF EXISTS "Members can update own profile" ON public.members;
|
|
|
|
|
DROP POLICY IF EXISTS "Admins can update other members" ON public.members;
|
|
|
|
|
|
|
|
|
|
CREATE POLICY "Members can update own profile"
|
|
|
|
|
ON public.members
|
|
|
|
|
FOR UPDATE
|
2026-02-06 10:33:25 +01:00
|
|
|
TO authenticated
|
|
|
|
|
USING (auth.uid() = id)
|
|
|
|
|
WITH CHECK (
|
|
|
|
|
auth.uid() = id
|
2026-02-10 18:11:02 +01:00
|
|
|
AND role = (SELECT role FROM public.members WHERE id = auth.uid())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE POLICY "Admins can update other members"
|
|
|
|
|
ON public.members
|
|
|
|
|
FOR UPDATE
|
|
|
|
|
TO authenticated
|
|
|
|
|
USING (
|
|
|
|
|
EXISTS (
|
|
|
|
|
SELECT 1 FROM public.members
|
|
|
|
|
WHERE id = auth.uid() AND role = 'admin'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
WITH CHECK (
|
|
|
|
|
(id != auth.uid())
|
|
|
|
|
OR
|
|
|
|
|
(id = auth.uid() AND role = (SELECT role FROM public.members WHERE id = auth.uid()))
|
2026-02-06 10:33:25 +01:00
|
|
|
);
|
|
|
|
|
|
2026-02-10 18:11:02 +01:00
|
|
|
-- --- Migration 020: Approval workflow ---
|
|
|
|
|
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
|
|
|
|
|
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS approved_by UUID REFERENCES auth.users(id);
|
|
|
|
|
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS rejected_at TIMESTAMPTZ;
|
|
|
|
|
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS rejected_by UUID REFERENCES auth.users(id);
|
|
|
|
|
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS rejection_reason TEXT;
|
|
|
|
|
|
|
|
|
|
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active)
|
|
|
|
|
VALUES
|
|
|
|
|
('member_approved', 'Member Approved', 'membership', 'Welcome to Monaco USA - Membership Approved!',
|
|
|
|
|
'<h2 style="color: #22c55e; text-align: center;">Membership Approved!</h2>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">We are pleased to inform you that your membership application to Monaco USA has been <strong>approved</strong>!</p>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">Your member ID is: <strong>{{member_id}}</strong></p>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">You now have full access to the member portal, including events, documents, and the member directory.</p>
|
|
|
|
|
<div style="text-align: center; margin: 24px 0;">
|
|
|
|
|
<a href="{{site_url}}/dashboard" style="background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: bold;">Visit Your Dashboard</a>
|
|
|
|
|
</div>',
|
|
|
|
|
'Dear {{first_name}}, Your membership to Monaco USA has been approved! Your member ID is {{member_id}}.',
|
|
|
|
|
true),
|
|
|
|
|
('member_rejected', 'Member Rejected', 'membership', 'Monaco USA - Membership Application Update',
|
|
|
|
|
'<h2 style="color: #ef4444; text-align: center;">Application Update</h2>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">Thank you for your interest in Monaco USA. After careful review, we regret to inform you that your membership application was not approved at this time.</p>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">{{reason}}</p>
|
|
|
|
|
<p style="color: #334155; line-height: 1.6;">If you have questions, please contact us at info@monacousa.org.</p>',
|
|
|
|
|
'Dear {{first_name}}, Thank you for your interest in Monaco USA. After review, your membership application was not approved at this time.',
|
|
|
|
|
true)
|
|
|
|
|
ON CONFLICT (template_key) DO NOTHING;
|
|
|
|
|
|
|
|
|
|
-- --- Migration 021: RSVP deadlines ---
|
|
|
|
|
ALTER TABLE public.events ADD COLUMN IF NOT EXISTS rsvp_deadline TIMESTAMPTZ;
|
|
|
|
|
ALTER TABLE public.events ADD COLUMN IF NOT EXISTS rsvp_deadline_enabled BOOLEAN DEFAULT FALSE;
|
|
|
|
|
|
|
|
|
|
-- --- Migration 022: Directory privacy ---
|
|
|
|
|
ALTER TABLE public.members
|
|
|
|
|
ADD COLUMN IF NOT EXISTS directory_privacy JSONB DEFAULT '{
|
|
|
|
|
"show_email": true,
|
|
|
|
|
"show_phone": true,
|
|
|
|
|
"show_address": false,
|
|
|
|
|
"show_nationality": true
|
|
|
|
|
}'::jsonb;
|
|
|
|
|
|
|
|
|
|
-- --- Migration 023: Document full-text search ---
|
|
|
|
|
ALTER TABLE public.documents
|
|
|
|
|
ADD COLUMN IF NOT EXISTS search_vector tsvector;
|
|
|
|
|
|
|
|
|
|
UPDATE public.documents
|
|
|
|
|
SET search_vector = to_tsvector('english',
|
|
|
|
|
coalesce(title, '') || ' ' ||
|
|
|
|
|
coalesce(description, '') || ' ' ||
|
|
|
|
|
coalesce(file_name, '')
|
|
|
|
|
)
|
|
|
|
|
WHERE search_vector IS NULL;
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_documents_search
|
|
|
|
|
ON public.documents USING GIN (search_vector);
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION update_document_search_vector()
|
|
|
|
|
RETURNS TRIGGER AS $$
|
|
|
|
|
BEGIN
|
|
|
|
|
NEW.search_vector := to_tsvector('english',
|
|
|
|
|
coalesce(NEW.title, '') || ' ' ||
|
|
|
|
|
coalesce(NEW.description, '') || ' ' ||
|
|
|
|
|
coalesce(NEW.file_name, '')
|
|
|
|
|
);
|
|
|
|
|
RETURN NEW;
|
|
|
|
|
END;
|
|
|
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
|
|
|
|
DROP TRIGGER IF EXISTS trg_update_document_search ON public.documents;
|
|
|
|
|
CREATE TRIGGER trg_update_document_search
|
|
|
|
|
BEFORE INSERT OR UPDATE OF title, description, file_name
|
|
|
|
|
ON public.documents
|
|
|
|
|
FOR EACH ROW
|
|
|
|
|
EXECUTE FUNCTION update_document_search_vector();
|
|
|
|
|
|
|
|
|
|
-- --- Migration 024: Cron execution logs ---
|
|
|
|
|
CREATE TABLE IF NOT EXISTS public.cron_execution_logs (
|
|
|
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
job_name TEXT NOT NULL,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
|
|
|
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
completed_at TIMESTAMPTZ,
|
|
|
|
|
duration_ms INTEGER,
|
|
|
|
|
result JSONB,
|
|
|
|
|
error_message TEXT,
|
|
|
|
|
triggered_by TEXT DEFAULT 'cron'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_cron_logs_job_name ON public.cron_execution_logs (job_name, started_at DESC);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_cron_logs_started ON public.cron_execution_logs (started_at DESC);
|
|
|
|
|
|
|
|
|
|
ALTER TABLE public.cron_execution_logs ENABLE ROW LEVEL SECURITY;
|
|
|
|
|
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.cron_execution_logs'::regclass AND polname = 'Admins can read cron logs') THEN
|
|
|
|
|
CREATE POLICY "Admins can read cron logs"
|
|
|
|
|
ON public.cron_execution_logs FOR SELECT TO authenticated
|
2026-02-10 19:13:44 +01:00
|
|
|
USING (EXISTS (SELECT 1 FROM public.members WHERE members.id = auth.uid() AND members.role = 'admin'));
|
2026-02-10 18:11:02 +01:00
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.cron_execution_logs'::regclass AND polname = 'Service role can manage cron logs') THEN
|
|
|
|
|
CREATE POLICY "Service role can manage cron logs"
|
|
|
|
|
ON public.cron_execution_logs FOR ALL TO service_role
|
|
|
|
|
USING (true) WITH CHECK (true);
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- --- Migration 025: Bulk email broadcasts ---
|
|
|
|
|
CREATE TABLE IF NOT EXISTS public.bulk_emails (
|
|
|
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
|
subject TEXT NOT NULL,
|
|
|
|
|
body TEXT NOT NULL,
|
|
|
|
|
recipient_filter JSONB DEFAULT '{"target": "all"}'::jsonb,
|
|
|
|
|
total_recipients INTEGER DEFAULT 0,
|
|
|
|
|
sent_count INTEGER DEFAULT 0,
|
|
|
|
|
failed_count INTEGER DEFAULT 0,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sending', 'completed', 'failed')),
|
|
|
|
|
sent_by UUID REFERENCES auth.users(id),
|
|
|
|
|
sent_by_name TEXT,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
sent_at TIMESTAMPTZ,
|
|
|
|
|
completed_at TIMESTAMPTZ
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bulk_emails_created ON public.bulk_emails (created_at DESC);
|
|
|
|
|
|
|
|
|
|
ALTER TABLE public.bulk_emails ENABLE ROW LEVEL SECURITY;
|
|
|
|
|
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.bulk_emails'::regclass AND polname = 'Admins can manage bulk emails') THEN
|
|
|
|
|
CREATE POLICY "Admins can manage bulk emails"
|
|
|
|
|
ON public.bulk_emails FOR ALL TO authenticated
|
2026-02-10 19:13:44 +01:00
|
|
|
USING (EXISTS (SELECT 1 FROM public.members WHERE members.id = auth.uid() AND members.role = 'admin'));
|
2026-02-10 18:11:02 +01:00
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid = 'public.bulk_emails'::regclass AND polname = 'Service role full access to bulk emails') THEN
|
|
|
|
|
CREATE POLICY "Service role full access to bulk emails"
|
|
|
|
|
ON public.bulk_emails FOR ALL TO service_role
|
|
|
|
|
USING (true) WITH CHECK (true);
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
GRANT ALL ON public.cron_execution_logs TO service_role;
|
|
|
|
|
GRANT ALL ON public.bulk_emails TO service_role;
|
|
|
|
|
|
2026-02-06 10:33:25 +01:00
|
|
|
-- ============================================
|
|
|
|
|
-- 5. ENSURE SERVICE_ROLE ACCESS TO ALL TABLES
|
|
|
|
|
-- ============================================
|
|
|
|
|
|
|
|
|
|
GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
|
|
|
|
|
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role;
|
|
|
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO service_role;
|
|
|
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role;
|
|
|
|
|
|
|
|
|
|
-- ============================================
|
|
|
|
|
-- DONE
|
|
|
|
|
-- ============================================
|
|
|
|
|
DO $$ BEGIN RAISE NOTICE '=== Post-deploy script completed successfully ==='; END $$;
|