Add migrations 018-025 to post-deploy.sql for automatic deployment
Build and Push Docker Images / build-portal (push) Successful in 3m5s
Details
Build and Push Docker Images / build-infra (docker/db, monacousa-db) (push) Successful in 1m3s
Details
Build and Push Docker Images / build-infra (docker/kong, monacousa-kong) (push) Successful in 22s
Details
Build and Push Docker Images / build-infra (docker/migrate, monacousa-migrate) (push) Successful in 1m1s
Details
Build and Push Docker Images / build-portal (push) Successful in 3m5s
Details
Build and Push Docker Images / build-infra (docker/db, monacousa-db) (push) Successful in 1m3s
Details
Build and Push Docker Images / build-infra (docker/kong, monacousa-kong) (push) Successful in 22s
Details
Build and Push Docker Images / build-infra (docker/migrate, monacousa-migrate) (push) Successful in 1m1s
Details
All new migrations are now embedded in post-deploy.sql (idempotent), so they run automatically on `docker compose up` via the migrate container. Both deploy/ and docker/migrate/ copies are kept in sync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5ff9f950a1
commit
f9364d2176
|
|
@ -198,19 +198,237 @@ CREATE TRIGGER on_member_created_notification
|
|||
EXECUTE FUNCTION create_welcome_notification();
|
||||
|
||||
-- ============================================
|
||||
-- 4. MIGRATION 017: Fix RLS role escalation
|
||||
-- 4. MIGRATIONS 017-025 (idempotent)
|
||||
-- ============================================
|
||||
|
||||
-- --- 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 ---
|
||||
DROP POLICY IF EXISTS "Users can update own profile" ON public.members;
|
||||
CREATE POLICY "Users can update own profile"
|
||||
ON public.members FOR UPDATE
|
||||
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
|
||||
TO authenticated
|
||||
USING (auth.uid() = id)
|
||||
WITH CHECK (
|
||||
auth.uid() = id
|
||||
AND role = (SELECT m.role FROM public.members m WHERE m.id = auth.uid())
|
||||
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()))
|
||||
);
|
||||
|
||||
-- --- 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
|
||||
USING (EXISTS (SELECT 1 FROM public.members WHERE members.user_id = auth.uid() AND members.role = 'admin'));
|
||||
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
|
||||
USING (EXISTS (SELECT 1 FROM public.members WHERE members.user_id = auth.uid() AND members.role = 'admin'));
|
||||
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;
|
||||
|
||||
-- ============================================
|
||||
-- 5. ENSURE SERVICE_ROLE ACCESS TO ALL TABLES
|
||||
-- ============================================
|
||||
|
|
|
|||
|
|
@ -198,19 +198,237 @@ CREATE TRIGGER on_member_created_notification
|
|||
EXECUTE FUNCTION create_welcome_notification();
|
||||
|
||||
-- ============================================
|
||||
-- 4. MIGRATION 017: Fix RLS role escalation
|
||||
-- 4. MIGRATIONS 017-025 (idempotent)
|
||||
-- ============================================
|
||||
|
||||
-- --- 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 ---
|
||||
DROP POLICY IF EXISTS "Users can update own profile" ON public.members;
|
||||
CREATE POLICY "Users can update own profile"
|
||||
ON public.members FOR UPDATE
|
||||
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
|
||||
TO authenticated
|
||||
USING (auth.uid() = id)
|
||||
WITH CHECK (
|
||||
auth.uid() = id
|
||||
AND role = (SELECT m.role FROM public.members m WHERE m.id = auth.uid())
|
||||
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()))
|
||||
);
|
||||
|
||||
-- --- 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
|
||||
USING (EXISTS (SELECT 1 FROM public.members WHERE members.user_id = auth.uid() AND members.role = 'admin'));
|
||||
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
|
||||
USING (EXISTS (SELECT 1 FROM public.members WHERE members.user_id = auth.uid() AND members.role = 'admin'));
|
||||
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;
|
||||
|
||||
-- ============================================
|
||||
-- 5. ENSURE SERVICE_ROLE ACCESS TO ALL TABLES
|
||||
-- ============================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue