feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
7
supabase/migrations/032_document_event_dept_fks.sql
Normal file
7
supabase/migrations/032_document_event_dept_fks.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Add event_id and department_id to documents for auto-folder linking
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS event_id uuid REFERENCES events(id) ON DELETE SET NULL;
|
||||
ALTER TABLE documents ADD COLUMN IF NOT EXISTS department_id uuid REFERENCES event_departments(id) ON DELETE SET NULL;
|
||||
|
||||
-- Index for quick lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_event_id ON documents(event_id) WHERE event_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_department_id ON documents(department_id) WHERE department_id IS NOT NULL;
|
||||
28
supabase/migrations/033_files_storage.sql
Normal file
28
supabase/migrations/033_files_storage.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Create storage bucket for uploaded files
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit)
|
||||
VALUES ('files', 'files', true, 52428800) -- 50MB max file size
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Allow authenticated users to upload files (organized by org_id/folder)
|
||||
CREATE POLICY "Authenticated users can upload files"
|
||||
ON storage.objects FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (bucket_id = 'files');
|
||||
|
||||
-- Allow authenticated users to update their files
|
||||
CREATE POLICY "Authenticated users can update files"
|
||||
ON storage.objects FOR UPDATE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'files');
|
||||
|
||||
-- Allow public read access to all files
|
||||
CREATE POLICY "Public read access for files"
|
||||
ON storage.objects FOR SELECT
|
||||
TO public
|
||||
USING (bucket_id = 'files');
|
||||
|
||||
-- Allow authenticated users to delete files
|
||||
CREATE POLICY "Authenticated users can delete files"
|
||||
ON storage.objects FOR DELETE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'files');
|
||||
5
supabase/migrations/034_layout_triple.sql
Normal file
5
supabase/migrations/034_layout_triple.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add 'triple' to layout_preset enum for 3-column layout
|
||||
ALTER TYPE layout_preset ADD VALUE IF NOT EXISTS 'triple';
|
||||
|
||||
-- Migrate any existing 'focus_sidebar' or 'custom' layouts to 'split'
|
||||
UPDATE department_dashboards SET layout = 'split' WHERE layout IN ('focus_sidebar', 'custom');
|
||||
7
supabase/migrations/035_budget_receipts.sql
Normal file
7
supabase/migrations/035_budget_receipts.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Add receipt/invoice document linking to budget items
|
||||
-- Each budget item can optionally link to a document (uploaded file) as its receipt/invoice
|
||||
|
||||
ALTER TABLE budget_items
|
||||
ADD COLUMN receipt_document_id UUID REFERENCES documents(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX idx_budget_items_receipt ON budget_items(receipt_document_id);
|
||||
210
supabase/migrations/036_finance_enhancements.sql
Normal file
210
supabase/migrations/036_finance_enhancements.sql
Normal file
@@ -0,0 +1,210 @@
|
||||
-- Finance enhancements: planned budgets per department, sponsor allocations,
|
||||
-- org-wide contacts, and department contact pinning
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Add planned_budget to event_departments
|
||||
-- ============================================================
|
||||
ALTER TABLE event_departments
|
||||
ADD COLUMN IF NOT EXISTS planned_budget NUMERIC(12,2) NOT NULL DEFAULT 0;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Sponsor Allocations
|
||||
-- Tracks how sponsor money is allocated to departments.
|
||||
-- A sponsor (confirmed/active) has an amount; the finance lead
|
||||
-- distributes portions of that to specific departments.
|
||||
-- ============================================================
|
||||
CREATE TABLE sponsor_allocations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sponsor_id UUID NOT NULL REFERENCES sponsors(id) ON DELETE CASCADE,
|
||||
department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
|
||||
allocated_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
used_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
notes TEXT,
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sponsor_alloc_sponsor ON sponsor_allocations(sponsor_id);
|
||||
CREATE INDEX idx_sponsor_alloc_dept ON sponsor_allocations(department_id);
|
||||
-- Prevent duplicate allocations of the same sponsor to the same department
|
||||
CREATE UNIQUE INDEX idx_sponsor_alloc_unique ON sponsor_allocations(sponsor_id, department_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. Org-wide Contacts
|
||||
-- Contacts that belong to the organization, not a department.
|
||||
-- Departments can "pin" contacts from this list.
|
||||
-- ============================================================
|
||||
CREATE TABLE org_contacts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
role TEXT,
|
||||
company TEXT,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
website TEXT,
|
||||
notes TEXT,
|
||||
category TEXT DEFAULT 'general',
|
||||
color TEXT DEFAULT '#00A3E0',
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_org_contacts_org ON org_contacts(org_id);
|
||||
CREATE INDEX idx_org_contacts_category ON org_contacts(category);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Department pinned contacts (junction table)
|
||||
-- Links org_contacts to departments for quick access
|
||||
-- ============================================================
|
||||
CREATE TABLE department_pinned_contacts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
|
||||
contact_id UUID NOT NULL REFERENCES org_contacts(id) ON DELETE CASCADE,
|
||||
pinned_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_dept_pinned_unique ON department_pinned_contacts(department_id, contact_id);
|
||||
CREATE INDEX idx_dept_pinned_dept ON department_pinned_contacts(department_id);
|
||||
CREATE INDEX idx_dept_pinned_contact ON department_pinned_contacts(contact_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. RLS Policies
|
||||
-- ============================================================
|
||||
|
||||
-- Sponsor Allocations
|
||||
ALTER TABLE sponsor_allocations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "sponsor_allocations_select" ON sponsor_allocations FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = sponsor_allocations.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "sponsor_allocations_insert" ON sponsor_allocations FOR INSERT
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = sponsor_allocations.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "sponsor_allocations_update" ON sponsor_allocations FOR UPDATE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = sponsor_allocations.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "sponsor_allocations_delete" ON sponsor_allocations FOR DELETE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = sponsor_allocations.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
-- Org Contacts
|
||||
ALTER TABLE org_contacts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "org_contacts_select" ON org_contacts FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM org_members om
|
||||
WHERE om.org_id = org_contacts.org_id
|
||||
AND om.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "org_contacts_insert" ON org_contacts FOR INSERT
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM org_members om
|
||||
WHERE om.org_id = org_contacts.org_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "org_contacts_update" ON org_contacts FOR UPDATE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM org_members om
|
||||
WHERE om.org_id = org_contacts.org_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "org_contacts_delete" ON org_contacts FOR DELETE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM org_members om
|
||||
WHERE om.org_id = org_contacts.org_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
-- Department Pinned Contacts
|
||||
ALTER TABLE department_pinned_contacts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "dept_pinned_contacts_select" ON department_pinned_contacts FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = department_pinned_contacts.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "dept_pinned_contacts_insert" ON department_pinned_contacts FOR INSERT
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = department_pinned_contacts.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "dept_pinned_contacts_delete" ON department_pinned_contacts FOR DELETE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON e.id = ed.event_id
|
||||
JOIN org_members om ON om.org_id = e.org_id
|
||||
WHERE ed.id = department_pinned_contacts.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND om.role IN ('owner', 'admin', 'editor')
|
||||
)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Enable realtime for sponsor allocations
|
||||
-- ============================================================
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE sponsor_allocations;
|
||||
34
supabase/migrations/037_org_settings.sql
Normal file
34
supabase/migrations/037_org_settings.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- Organization settings: currency, locale, defaults, feature toggles, branding
|
||||
-- Adds configurable preferences to the organizations table
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Preferences (currency, locale, calendar)
|
||||
-- ============================================================
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS currency TEXT NOT NULL DEFAULT 'EUR';
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY';
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS timezone TEXT NOT NULL DEFAULT 'Europe/Tallinn';
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS week_start_day TEXT NOT NULL DEFAULT 'monday'
|
||||
CHECK (week_start_day IN ('monday', 'sunday'));
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_calendar_view TEXT NOT NULL DEFAULT 'month'
|
||||
CHECK (default_calendar_view IN ('month', 'week', 'day'));
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Event defaults
|
||||
-- ============================================================
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_event_color TEXT NOT NULL DEFAULT '#7986cb';
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_event_status TEXT NOT NULL DEFAULT 'planning';
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_dept_modules TEXT[] NOT NULL DEFAULT ARRAY['kanban', 'files', 'checklist'];
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_dept_layout TEXT NOT NULL DEFAULT 'split';
|
||||
|
||||
-- ============================================================
|
||||
-- 3. Feature toggles
|
||||
-- ============================================================
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_chat BOOLEAN NOT NULL DEFAULT true;
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_sponsors BOOLEAN NOT NULL DEFAULT true;
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_contacts BOOLEAN NOT NULL DEFAULT true;
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_budget BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Branding / description
|
||||
-- ============================================================
|
||||
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS description TEXT DEFAULT '';
|
||||
5
supabase/migrations/038_social_links.sql
Normal file
5
supabase/migrations/038_social_links.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add social_links JSONB column to organizations
|
||||
ALTER TABLE public.organizations
|
||||
ADD COLUMN IF NOT EXISTS social_links jsonb DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN public.organizations.social_links IS 'JSON object storing social media URLs: website, instagram, facebook, discord, linkedin, x, youtube, tiktok, fienta';
|
||||
368
supabase/migrations/039_security_performance_fixes.sql
Normal file
368
supabase/migrations/039_security_performance_fixes.sql
Normal file
@@ -0,0 +1,368 @@
|
||||
-- ============================================================
|
||||
-- Migration 039: Security & Performance fixes from Supabase linter reports
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. CRITICAL: Enable RLS on organizations table (policies exist but RLS was off)
|
||||
-- ============================================================
|
||||
ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. Fix mutable search_path on all public functions
|
||||
-- Adds SET search_path = '' to make them immutable to search_path attacks
|
||||
-- ============================================================
|
||||
|
||||
-- is_platform_admin
|
||||
CREATE OR REPLACE FUNCTION public.is_platform_admin()
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.profiles
|
||||
WHERE id = (select auth.uid()) AND is_platform_admin = true
|
||||
);
|
||||
$$;
|
||||
|
||||
-- is_org_member (used heavily in RLS)
|
||||
CREATE OR REPLACE FUNCTION public.is_org_member(org_id uuid)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.org_members
|
||||
WHERE org_members.org_id = is_org_member.org_id
|
||||
AND user_id = (select auth.uid())
|
||||
);
|
||||
$$;
|
||||
|
||||
-- handle_new_user
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (id, email, full_name, avatar_url)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.email,
|
||||
COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
|
||||
NEW.raw_user_meta_data->>'avatar_url'
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- handle_new_org
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_org()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Add creator as owner
|
||||
INSERT INTO public.org_members (org_id, user_id, role)
|
||||
VALUES (NEW.id, NEW.created_by, 'owner');
|
||||
-- Create default roles
|
||||
PERFORM public.create_default_org_roles(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- create_default_org_roles
|
||||
CREATE OR REPLACE FUNCTION public.create_default_org_roles(p_org_id uuid)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.org_roles (org_id, name, color, permissions, is_system, is_default)
|
||||
VALUES
|
||||
(p_org_id, 'Admin', '#ef4444', ARRAY['manage_members','manage_roles','manage_settings','manage_content','view_content','comment'], true, false),
|
||||
(p_org_id, 'Editor', '#3b82f6', ARRAY['manage_content','view_content','comment'], true, true),
|
||||
(p_org_id, 'Commenter', '#8b5cf6', ARRAY['view_content','comment'], true, false),
|
||||
(p_org_id, 'Viewer', '#6b7280', ARRAY['view_content'], true, false)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- compute_document_path
|
||||
CREATE OR REPLACE FUNCTION public.compute_document_path(doc_id uuid)
|
||||
RETURNS text
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
result text := '';
|
||||
current_id uuid := doc_id;
|
||||
current_name text;
|
||||
parent uuid;
|
||||
BEGIN
|
||||
LOOP
|
||||
SELECT d.name, d.parent_id INTO current_name, parent
|
||||
FROM public.documents d WHERE d.id = current_id;
|
||||
IF NOT FOUND THEN EXIT; END IF;
|
||||
IF result = '' THEN result := current_name;
|
||||
ELSE result := current_name || '/' || result;
|
||||
END IF;
|
||||
IF parent IS NULL THEN EXIT; END IF;
|
||||
current_id := parent;
|
||||
END LOOP;
|
||||
RETURN '/' || result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- update_document_path
|
||||
CREATE OR REPLACE FUNCTION public.update_document_path()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.path := public.compute_document_path(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- get_next_document_position
|
||||
CREATE OR REPLACE FUNCTION public.get_next_document_position(p_org_id uuid, p_parent_id uuid)
|
||||
RETURNS integer
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
max_pos integer;
|
||||
BEGIN
|
||||
IF p_parent_id IS NULL THEN
|
||||
SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
|
||||
FROM public.documents WHERE org_id = p_org_id AND parent_id IS NULL;
|
||||
ELSE
|
||||
SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
|
||||
FROM public.documents WHERE org_id = p_org_id AND parent_id = p_parent_id;
|
||||
END IF;
|
||||
RETURN max_pos;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- update_matrix_credentials_updated_at
|
||||
CREATE OR REPLACE FUNCTION public.update_matrix_credentials_updated_at()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- handle_new_event
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_event()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM public.seed_event_task_columns(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- seed_event_task_columns
|
||||
CREATE OR REPLACE FUNCTION public.seed_event_task_columns(p_event_id uuid)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.event_task_columns (event_id, name, position, color)
|
||||
VALUES
|
||||
(p_event_id, 'To Do', 0, '#6b7280'),
|
||||
(p_event_id, 'In Progress', 1, '#3b82f6'),
|
||||
(p_event_id, 'Done', 2, '#22c55e')
|
||||
ON CONFLICT DO NOTHING;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- create_department_dashboard
|
||||
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
dash_id uuid;
|
||||
checklist_id uuid;
|
||||
note_id uuid;
|
||||
BEGIN
|
||||
INSERT INTO public.department_dashboards (department_id, created_by)
|
||||
VALUES (NEW.id, NEW.created_by)
|
||||
RETURNING id INTO dash_id;
|
||||
|
||||
INSERT INTO public.dashboard_panels (dashboard_id, module_type, position, config)
|
||||
VALUES
|
||||
(dash_id, 'kanban', 0, '{}'),
|
||||
(dash_id, 'files', 1, '{}'),
|
||||
(dash_id, 'checklist', 2, '{}'),
|
||||
(dash_id, 'notes', 3, '{}');
|
||||
|
||||
INSERT INTO public.department_checklists (department_id, title, created_by)
|
||||
VALUES (NEW.id, 'Getting Started', NEW.created_by)
|
||||
RETURNING id INTO checklist_id;
|
||||
|
||||
INSERT INTO public.department_checklist_items (checklist_id, title, position)
|
||||
VALUES
|
||||
(checklist_id, 'Set up department goals', 0),
|
||||
(checklist_id, 'Assign team members', 1),
|
||||
(checklist_id, 'Create initial tasks', 2);
|
||||
|
||||
INSERT INTO public.department_notes (department_id, title, content, created_by)
|
||||
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', NEW.created_by);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. Fix overly permissive RLS policy: org_members "Allow member inserts"
|
||||
-- Replace WITH CHECK (true) with proper check
|
||||
-- ============================================================
|
||||
DROP POLICY IF EXISTS "Allow member inserts" ON public.org_members;
|
||||
CREATE POLICY "Allow member inserts" ON public.org_members
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
-- Only allow if user is an admin/owner of the org, or it's the system (via trigger)
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.org_members om
|
||||
WHERE om.org_id = org_members.org_id
|
||||
AND om.user_id = (select auth.uid())
|
||||
AND om.role IN ('owner', 'admin')
|
||||
)
|
||||
OR
|
||||
-- Allow the handle_new_org trigger (creator becomes owner)
|
||||
org_members.user_id = (select auth.uid())
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Add missing RLS policies for tables with RLS enabled but no policies
|
||||
-- ============================================================
|
||||
|
||||
-- card_assignees: inherit access from the card's board's org
|
||||
CREATE POLICY "Card assignees inherit card access" ON public.card_assignees
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.kanban_cards kc
|
||||
JOIN public.kanban_columns kcol ON kcol.id = kc.column_id
|
||||
JOIN public.kanban_boards kb ON kb.id = kcol.board_id
|
||||
JOIN public.org_members om ON om.org_id = kb.org_id
|
||||
WHERE kc.id = card_assignees.card_id
|
||||
AND om.user_id = (select auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
-- event_attendees: org members can access
|
||||
CREATE POLICY "Org members can manage event attendees" ON public.event_attendees
|
||||
FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.events e
|
||||
JOIN public.org_members om ON om.org_id = e.org_id
|
||||
WHERE e.id = event_attendees.event_id
|
||||
AND om.user_id = (select auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 5. Add indexes on unindexed foreign keys (created_by, user_id, role_id, org_id)
|
||||
-- ============================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_budget_items_created_by ON public.budget_items (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_events_created_by ON public.calendar_events (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_card_assignees_user_id ON public.card_assignees (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_department_checklists_created_by ON public.department_checklists (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_department_contacts_created_by ON public.department_contacts (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_department_dashboards_created_by ON public.department_dashboards (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_department_notes_created_by ON public.department_notes (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_document_locks_user_id ON public.document_locks (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_created_by ON public.documents (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_attendees_user_id ON public.event_attendees (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_members_role_id ON public.event_members (role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_tasks_created_by ON public.event_tasks (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_by ON public.events (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_kanban_boards_created_by ON public.kanban_boards (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_kanban_cards_created_by ON public.kanban_cards (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_kanban_checklist_items_card_id ON public.kanban_checklist_items (card_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_matrix_credentials_org_id ON public.matrix_credentials (org_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_contacts_created_by ON public.org_contacts (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_google_calendars_connected_by ON public.org_google_calendars (connected_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_invites_invited_by ON public.org_invites (invited_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_invites_role_id ON public.org_invites (role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_org_members_role_id ON public.org_members (role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_blocks_created_by ON public.schedule_blocks (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_sponsor_allocations_created_by ON public.sponsor_allocations (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_sponsors_created_by ON public.sponsors (created_by);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 6. Drop unused indexes (never been used per Supabase linter)
|
||||
-- ============================================================
|
||||
|
||||
DROP INDEX IF EXISTS public.idx_kanban_cards_assignee;
|
||||
DROP INDEX IF EXISTS public.idx_kanban_cards_due_date;
|
||||
DROP INDEX IF EXISTS public.idx_calendar_events_google;
|
||||
DROP INDEX IF EXISTS public.idx_activity_log_entity;
|
||||
DROP INDEX IF EXISTS public.idx_team_members_team;
|
||||
DROP INDEX IF EXISTS public.idx_team_members_user;
|
||||
DROP INDEX IF EXISTS public.idx_kanban_boards_team;
|
||||
DROP INDEX IF EXISTS public.idx_kanban_boards_personal;
|
||||
DROP INDEX IF EXISTS public.idx_kanban_comments_user;
|
||||
DROP INDEX IF EXISTS public.idx_kanban_comments_created;
|
||||
DROP INDEX IF EXISTS public.idx_document_locks_heartbeat;
|
||||
DROP INDEX IF EXISTS public.idx_event_members_user;
|
||||
DROP INDEX IF EXISTS public.idx_events_org;
|
||||
DROP INDEX IF EXISTS public.idx_event_tasks_assignee;
|
||||
DROP INDEX IF EXISTS public.idx_dept_checklist_items_assigned;
|
||||
DROP INDEX IF EXISTS public.idx_schedule_blocks_time;
|
||||
DROP INDEX IF EXISTS public.idx_department_contacts_category;
|
||||
DROP INDEX IF EXISTS public.idx_budget_items_category;
|
||||
DROP INDEX IF EXISTS public.idx_sponsors_tier;
|
||||
DROP INDEX IF EXISTS public.idx_sponsors_status;
|
||||
DROP INDEX IF EXISTS public.idx_sponsor_alloc_sponsor;
|
||||
DROP INDEX IF EXISTS public.idx_org_contacts_category;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 7. NOTE: "Leaked password protection" is an Auth dashboard setting,
|
||||
-- not fixable via migration. Enable it in:
|
||||
-- Supabase Dashboard → Authentication → Settings → Password Security
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 8. NOTE: RLS initplan warnings (auth.uid() → (select auth.uid()))
|
||||
-- There are ~90 policies that use auth.uid() directly instead of
|
||||
-- (select auth.uid()). This causes per-row re-evaluation.
|
||||
-- The fix is mechanical: wrap auth.uid() in a subselect.
|
||||
-- This is addressed in the new policies above. For existing policies
|
||||
-- across all tables, a comprehensive rewrite would be very large.
|
||||
-- We recommend addressing these incrementally per table as needed,
|
||||
-- or in a dedicated follow-up migration.
|
||||
-- ============================================================
|
||||
2
supabase/migrations/040_map_module.sql
Normal file
2
supabase/migrations/040_map_module.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add 'map' to the module_type enum
|
||||
ALTER TYPE module_type ADD VALUE IF NOT EXISTS 'map';
|
||||
42
supabase/migrations/041_fix_department_dashboard_trigger.sql
Normal file
42
supabase/migrations/041_fix_department_dashboard_trigger.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- Fix: event_departments has no created_by column.
|
||||
-- Use auth.uid() instead of NEW.created_by in the auto-create trigger.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
dash_id uuid;
|
||||
checklist_id uuid;
|
||||
note_id uuid;
|
||||
current_user_id uuid := auth.uid();
|
||||
BEGIN
|
||||
INSERT INTO public.department_dashboards (department_id, created_by)
|
||||
VALUES (NEW.id, current_user_id)
|
||||
RETURNING id INTO dash_id;
|
||||
|
||||
INSERT INTO public.dashboard_panels (dashboard_id, module_type, position, config)
|
||||
VALUES
|
||||
(dash_id, 'kanban', 0, '{}'),
|
||||
(dash_id, 'files', 1, '{}'),
|
||||
(dash_id, 'checklist', 2, '{}'),
|
||||
(dash_id, 'notes', 3, '{}');
|
||||
|
||||
INSERT INTO public.department_checklists (department_id, title, created_by)
|
||||
VALUES (NEW.id, 'Getting Started', current_user_id)
|
||||
RETURNING id INTO checklist_id;
|
||||
|
||||
INSERT INTO public.department_checklist_items (checklist_id, title, position)
|
||||
VALUES
|
||||
(checklist_id, 'Set up department goals', 0),
|
||||
(checklist_id, 'Assign team members', 1),
|
||||
(checklist_id, 'Create initial tasks', 2);
|
||||
|
||||
INSERT INTO public.department_notes (department_id, title, content, created_by)
|
||||
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', current_user_id);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
41
supabase/migrations/042_fix_dashboard_panel_column.sql
Normal file
41
supabase/migrations/042_fix_dashboard_panel_column.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Fix: dashboard_panels column is "module" not "module_type"
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
dash_id uuid;
|
||||
checklist_id uuid;
|
||||
note_id uuid;
|
||||
current_user_id uuid := auth.uid();
|
||||
BEGIN
|
||||
INSERT INTO public.department_dashboards (department_id, created_by)
|
||||
VALUES (NEW.id, current_user_id)
|
||||
RETURNING id INTO dash_id;
|
||||
|
||||
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
|
||||
VALUES
|
||||
(dash_id, 'kanban', 0, '1'),
|
||||
(dash_id, 'files', 1, '1'),
|
||||
(dash_id, 'checklist', 2, '1'),
|
||||
(dash_id, 'notes', 3, '1');
|
||||
|
||||
INSERT INTO public.department_checklists (department_id, title, created_by)
|
||||
VALUES (NEW.id, 'Getting Started', current_user_id)
|
||||
RETURNING id INTO checklist_id;
|
||||
|
||||
INSERT INTO public.department_checklist_items (checklist_id, title, position)
|
||||
VALUES
|
||||
(checklist_id, 'Set up department goals', 0),
|
||||
(checklist_id, 'Assign team members', 1),
|
||||
(checklist_id, 'Create initial tasks', 2);
|
||||
|
||||
INSERT INTO public.department_notes (department_id, title, content, created_by)
|
||||
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', current_user_id);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
41
supabase/migrations/043_fix_dashboard_panel_width.sql
Normal file
41
supabase/migrations/043_fix_dashboard_panel_width.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Fix: width must be 'full'|'half'|'third'|'two_thirds', not '1'
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
dash_id uuid;
|
||||
checklist_id uuid;
|
||||
note_id uuid;
|
||||
current_user_id uuid := auth.uid();
|
||||
BEGIN
|
||||
INSERT INTO public.department_dashboards (department_id, created_by)
|
||||
VALUES (NEW.id, current_user_id)
|
||||
RETURNING id INTO dash_id;
|
||||
|
||||
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
|
||||
VALUES
|
||||
(dash_id, 'kanban', 0, 'half'),
|
||||
(dash_id, 'files', 1, 'half'),
|
||||
(dash_id, 'checklist', 2, 'half'),
|
||||
(dash_id, 'notes', 3, 'half');
|
||||
|
||||
INSERT INTO public.department_checklists (department_id, title, created_by)
|
||||
VALUES (NEW.id, 'Getting Started', current_user_id)
|
||||
RETURNING id INTO checklist_id;
|
||||
|
||||
INSERT INTO public.department_checklist_items (checklist_id, title, position)
|
||||
VALUES
|
||||
(checklist_id, 'Set up department goals', 0),
|
||||
(checklist_id, 'Assign team members', 1),
|
||||
(checklist_id, 'Create initial tasks', 2);
|
||||
|
||||
INSERT INTO public.department_notes (department_id, title, content, created_by)
|
||||
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', current_user_id);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
40
supabase/migrations/044_fix_checklist_item_columns.sql
Normal file
40
supabase/migrations/044_fix_checklist_item_columns.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- Fix: department_checklist_items uses "content" and "sort_order", not "title" and "position"
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
dash_id uuid;
|
||||
checklist_id uuid;
|
||||
current_user_id uuid := auth.uid();
|
||||
BEGIN
|
||||
INSERT INTO public.department_dashboards (department_id, created_by)
|
||||
VALUES (NEW.id, current_user_id)
|
||||
RETURNING id INTO dash_id;
|
||||
|
||||
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
|
||||
VALUES
|
||||
(dash_id, 'kanban', 0, 'half'),
|
||||
(dash_id, 'files', 1, 'half'),
|
||||
(dash_id, 'checklist', 2, 'half'),
|
||||
(dash_id, 'notes', 3, 'half');
|
||||
|
||||
INSERT INTO public.department_checklists (department_id, title, sort_order, created_by)
|
||||
VALUES (NEW.id, 'Getting Started', 0, current_user_id)
|
||||
RETURNING id INTO checklist_id;
|
||||
|
||||
INSERT INTO public.department_checklist_items (checklist_id, content, sort_order)
|
||||
VALUES
|
||||
(checklist_id, 'Set up department goals', 0),
|
||||
(checklist_id, 'Assign team members', 1),
|
||||
(checklist_id, 'Create initial tasks', 2);
|
||||
|
||||
INSERT INTO public.department_notes (department_id, title, content, sort_order, created_by)
|
||||
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', 0, current_user_id);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
29
supabase/migrations/045_fix_trigger_respect_modules.sql
Normal file
29
supabase/migrations/045_fix_trigger_respect_modules.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Fix: only create dashboard + panels for the modules the user selected (enabled_modules).
|
||||
-- No example checklists, notes, or other seed data.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
dash_id uuid;
|
||||
i int := 0;
|
||||
mod text;
|
||||
BEGIN
|
||||
-- Create the dashboard
|
||||
INSERT INTO public.department_dashboards (department_id, created_by)
|
||||
VALUES (NEW.id, auth.uid())
|
||||
RETURNING id INTO dash_id;
|
||||
|
||||
-- Create a panel for each enabled module
|
||||
FOREACH mod IN ARRAY NEW.enabled_modules LOOP
|
||||
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
|
||||
VALUES (dash_id, mod::public.module_type, i, 'half');
|
||||
i := i + 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
108
supabase/migrations/046_map_layers_pins.sql
Normal file
108
supabase/migrations/046_map_layers_pins.sql
Normal file
@@ -0,0 +1,108 @@
|
||||
-- ============================================================
|
||||
-- Map Layers & Pins: persistent map data for department map module
|
||||
-- ============================================================
|
||||
|
||||
-- 1. Map Layers (each department can have multiple map layers)
|
||||
CREATE TABLE map_layers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL DEFAULT 'Map',
|
||||
layer_type TEXT NOT NULL DEFAULT 'osm' CHECK (layer_type IN ('osm', 'image')),
|
||||
image_url TEXT,
|
||||
image_width INT,
|
||||
image_height INT,
|
||||
center_lat DOUBLE PRECISION DEFAULT 0,
|
||||
center_lng DOUBLE PRECISION DEFAULT 0,
|
||||
zoom_level INT DEFAULT 13,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_map_layers_dept ON map_layers(department_id);
|
||||
|
||||
-- 2. Map Pins (belong to a layer)
|
||||
CREATE TABLE map_pins (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
layer_id UUID NOT NULL REFERENCES map_layers(id) ON DELETE CASCADE,
|
||||
label TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
color TEXT NOT NULL DEFAULT '#EF4444',
|
||||
lat DOUBLE PRECISION NOT NULL,
|
||||
lng DOUBLE PRECISION NOT NULL,
|
||||
bounds_north DOUBLE PRECISION,
|
||||
bounds_south DOUBLE PRECISION,
|
||||
bounds_east DOUBLE PRECISION,
|
||||
bounds_west DOUBLE PRECISION,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_map_pins_layer ON map_pins(layer_id);
|
||||
|
||||
-- 3. RLS
|
||||
ALTER TABLE map_layers ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE map_pins ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Map Layers: org members can view
|
||||
CREATE POLICY "Org members can view map layers" ON map_layers FOR SELECT
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ed.id = map_layers.department_id AND om.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Map Layers: dept members + editors can manage
|
||||
CREATE POLICY "Dept members and editors can manage map layers" ON map_layers FOR ALL
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ed.id = map_layers.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
-- Map Pins: org members can view
|
||||
CREATE POLICY "Org members can view map pins" ON map_pins FOR SELECT
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_pins.layer_id AND om.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Map Pins: dept members + editors can manage
|
||||
CREATE POLICY "Dept members and editors can manage map pins" ON map_pins FOR ALL
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_pins.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
-- 4. Realtime
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE map_layers;
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE map_pins;
|
||||
148
supabase/migrations/047_fix_map_rls.sql
Normal file
148
supabase/migrations/047_fix_map_rls.sql
Normal file
@@ -0,0 +1,148 @@
|
||||
-- ============================================================
|
||||
-- Fix Map Layers & Pins RLS: replace FOR ALL with explicit
|
||||
-- INSERT/UPDATE/DELETE policies that include WITH CHECK clauses
|
||||
-- ============================================================
|
||||
|
||||
-- Drop the broken FOR ALL policies
|
||||
DROP POLICY IF EXISTS "Dept members and editors can manage map layers" ON map_layers;
|
||||
DROP POLICY IF EXISTS "Dept members and editors can manage map pins" ON map_pins;
|
||||
|
||||
-- ── Map Layers ──
|
||||
|
||||
CREATE POLICY "Dept members and editors can insert map layers" ON map_layers FOR INSERT
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ed.id = map_layers.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Dept members and editors can update map layers" ON map_layers FOR UPDATE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ed.id = map_layers.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
))
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ed.id = map_layers.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Dept members and editors can delete map layers" ON map_layers FOR DELETE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM event_departments ed
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ed.id = map_layers.department_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
-- ── Map Pins ──
|
||||
|
||||
CREATE POLICY "Dept members and editors can insert map pins" ON map_pins FOR INSERT
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_pins.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Dept members and editors can update map pins" ON map_pins FOR UPDATE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_pins.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
))
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_pins.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY "Dept members and editors can delete map pins" ON map_pins FOR DELETE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_pins.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
109
supabase/migrations/048_map_shapes.sql
Normal file
109
supabase/migrations/048_map_shapes.sql
Normal file
@@ -0,0 +1,109 @@
|
||||
-- ============================================================
|
||||
-- Map Shapes: polygons and rectangles for map layers
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE map_shapes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
layer_id UUID NOT NULL REFERENCES map_layers(id) ON DELETE CASCADE,
|
||||
shape_type TEXT NOT NULL DEFAULT 'polygon' CHECK (shape_type IN ('polygon', 'rectangle')),
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
color TEXT NOT NULL DEFAULT '#6366f1',
|
||||
fill_opacity DOUBLE PRECISION NOT NULL DEFAULT 0.15,
|
||||
stroke_width DOUBLE PRECISION NOT NULL DEFAULT 2,
|
||||
vertices JSONB NOT NULL DEFAULT '[]',
|
||||
rotation DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_map_shapes_layer ON map_shapes(layer_id);
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE map_shapes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Select: org members can view
|
||||
CREATE POLICY "Org members can view map shapes" ON map_shapes FOR SELECT
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_shapes.layer_id AND om.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Insert
|
||||
CREATE POLICY "Dept members and editors can insert map shapes" ON map_shapes FOR INSERT
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_shapes.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
-- Update
|
||||
CREATE POLICY "Dept members and editors can update map shapes" ON map_shapes FOR UPDATE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_shapes.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
))
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_shapes.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
-- Delete
|
||||
CREATE POLICY "Dept members and editors can delete map shapes" ON map_shapes FOR DELETE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM map_layers ml
|
||||
JOIN event_departments ed ON ml.department_id = ed.id
|
||||
JOIN events e ON ed.event_id = e.id
|
||||
JOIN org_members om ON e.org_id = om.org_id
|
||||
WHERE ml.id = map_shapes.layer_id
|
||||
AND om.user_id = auth.uid()
|
||||
AND (
|
||||
om.role IN ('owner', 'admin', 'editor')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM event_member_departments emd
|
||||
JOIN event_members em ON emd.event_member_id = em.id
|
||||
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE map_shapes;
|
||||
Reference in New Issue
Block a user