-- Monaco USA Portal 2026 -- Migration 003: Storage Buckets and Audit Logging -- ================================================ -- ============================================ -- STORAGE BUCKETS -- ============================================ -- Documents bucket (public for direct URL access - visibility controlled at app level) INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES ( 'documents', 'documents', true, 52428800, -- 50MB 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; -- Avatars bucket (public for display) INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES ( 'avatars', 'avatars', true, 5242880, -- 5MB 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; -- Event images bucket (public for display) INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES ( 'event-images', 'event-images', true, 10485760, -- 10MB 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; -- ============================================ -- STORAGE POLICIES -- ============================================ -- Documents bucket policies DROP POLICY IF EXISTS "documents_read_policy" ON storage.objects; CREATE POLICY "documents_read_policy" ON storage.objects FOR SELECT USING (bucket_id = 'documents' AND auth.role() = 'authenticated'); DROP POLICY IF EXISTS "documents_insert_policy" ON storage.objects; CREATE POLICY "documents_insert_policy" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'documents' AND auth.role() = 'authenticated' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin') ) ); DROP POLICY IF EXISTS "documents_delete_policy" ON storage.objects; CREATE POLICY "documents_delete_policy" ON storage.objects FOR DELETE USING ( bucket_id = 'documents' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin' ) ); -- Avatars bucket policies (public read, user-specific write) DROP POLICY IF EXISTS "avatars_read_policy" ON storage.objects; CREATE POLICY "avatars_read_policy" ON storage.objects FOR SELECT USING (bucket_id = 'avatars'); DROP POLICY IF EXISTS "avatars_insert_policy" ON storage.objects; CREATE POLICY "avatars_insert_policy" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'avatars' AND auth.role() = 'authenticated' AND (storage.foldername(name))[1] = auth.uid()::text ); DROP POLICY IF EXISTS "avatars_update_policy" ON storage.objects; CREATE POLICY "avatars_update_policy" ON storage.objects FOR UPDATE USING ( bucket_id = 'avatars' AND auth.role() = 'authenticated' AND (storage.foldername(name))[1] = auth.uid()::text ); DROP POLICY IF EXISTS "avatars_delete_policy" ON storage.objects; CREATE POLICY "avatars_delete_policy" ON storage.objects FOR DELETE USING ( bucket_id = 'avatars' AND auth.role() = 'authenticated' AND (storage.foldername(name))[1] = auth.uid()::text ); -- Event images bucket policies DROP POLICY IF EXISTS "event_images_read_policy" ON storage.objects; CREATE POLICY "event_images_read_policy" ON storage.objects FOR SELECT USING (bucket_id = 'event-images'); DROP POLICY IF EXISTS "event_images_insert_policy" ON storage.objects; CREATE POLICY "event_images_insert_policy" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'event-images' AND auth.role() = 'authenticated' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin') ) ); DROP POLICY IF EXISTS "event_images_delete_policy" ON storage.objects; CREATE POLICY "event_images_delete_policy" ON storage.objects FOR DELETE USING ( bucket_id = 'event-images' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin') ) ); -- ============================================ -- AUDIT LOGS TABLE -- ============================================ CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, user_email TEXT, action TEXT NOT NULL, resource_type TEXT, resource_id TEXT, details JSONB DEFAULT '{}', ip_address TEXT, user_agent TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Index for querying audit logs CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_type ON audit_logs(resource_type); CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at DESC); -- RLS for audit logs (only admins can read, service role can write) ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "audit_logs_read_admin" ON audit_logs; CREATE POLICY "audit_logs_read_admin" ON audit_logs FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin' ) ); -- ============================================ -- DEFAULT EMAIL TEMPLATES -- ============================================ -- Insert default email templates if they don't exist -- Using Monaco-branded design matching the login screen INSERT INTO email_templates (template_key, template_name, category, subject, body_html, body_text, is_system) VALUES ( 'welcome', 'Welcome Email', 'member', 'Welcome to Monaco USA, {{first_name}}!', '
Monaco USA

Monaco USA

Americans in Monaco

Welcome to Monaco USA!

Dear {{first_name}},

We are thrilled to welcome you to the Monaco USA community! Your membership has been created and you can now access all member features.

To get started:

  1. Set up your password using the separate email we sent
  2. Complete your profile with your details
  3. Explore upcoming events and connect with fellow members

If you have any questions, please don''t hesitate to reach out to our board members.

Best regards,
The Monaco USA Team

© 2026 Monaco USA. All rights reserved.

', 'Welcome to Monaco USA, {{first_name}}! Your membership has been created. Please set up your password and complete your profile.', true ), ( 'waitlist_promotion', 'Waitlist Promotion', 'event', 'Great news! You''re confirmed for {{event_title}}', '
Monaco USA

Monaco USA

Americans in Monaco

You''re In!

Dear {{first_name}},

Great news! A spot has opened up for {{event_title}} and you have been moved from the waitlist to confirmed!

Event Details

Date: {{event_date}}

Location: {{event_location}}

We look forward to seeing you there!

Best regards,
The Monaco USA Team

© 2026 Monaco USA. All rights reserved.

', 'Great news! A spot has opened up for {{event_title}} and you''ve been confirmed. See you on {{event_date}} at {{event_location}}!', true ), ( 'rsvp_confirmation', 'RSVP Confirmation', 'event', 'RSVP Confirmed: {{event_title}}', '
Monaco USA

Monaco USA

Americans in Monaco

RSVP Confirmed!

Dear {{first_name}},

Your RSVP for {{event_title}} has been confirmed.

Event Details

Date: {{event_date}}

Time: {{event_time}}

Location: {{event_location}}

Guests: {{guest_count}}

We look forward to seeing you!

Best regards,
The Monaco USA Team

© 2026 Monaco USA. All rights reserved.

', 'Your RSVP for {{event_title}} is confirmed! See you on {{event_date}} at {{event_location}}.', true ), ( 'payment_received', 'Payment Received', 'dues', 'Payment Received - Monaco USA', '
Monaco USA

Monaco USA

Americans in Monaco

Payment Received

Dear {{first_name}},

We have received your payment. Thank you!

Payment Details

Amount: ${{amount}}

Date: {{payment_date}}

Reference: {{reference}}

Your membership dues are now paid through {{due_date}}.

Thank you for your continued support of Monaco USA!

Best regards,
The Monaco USA Team

© 2026 Monaco USA. All rights reserved.

', 'Payment of ${{amount}} received on {{payment_date}}. Your dues are paid through {{due_date}}. Thank you!', true ), ( 'dues_reminder', 'Dues Reminder', 'dues', 'Monaco USA Membership Dues Reminder', '
Monaco USA

Monaco USA

Americans in Monaco

Membership Dues Reminder

Dear {{first_name}},

This is a friendly reminder that your Monaco USA membership dues {{status}}.

Dues Details

Amount Due: ${{amount}}

Due Date: {{due_date}}

Please log in to your member portal to view payment instructions or contact the treasurer for assistance.

Thank you for your continued membership!

Best regards,
The Monaco USA Team

© 2026 Monaco USA. All rights reserved.

', 'Reminder: Your Monaco USA membership dues ({{amount}}) {{status}}. Due date: {{due_date}}. Please log in to your portal for payment instructions.', true ) ON CONFLICT (template_key) DO NOTHING; -- Grant permissions GRANT SELECT, INSERT, UPDATE, DELETE ON audit_logs TO authenticated; GRANT ALL ON audit_logs TO service_role;