-- 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
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:
- Set up your password using the separate email we sent
- Complete your profile with your details
- 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
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
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
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
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;