Initial state for GitNexus analysis
This commit is contained in:
448
apps/web/supabase/migrations/20260401000001_cms_foundation.sql
Normal file
448
apps/web/supabase/migrations/20260401000001_cms_foundation.sql
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* CMS Foundation Schema
|
||||
* Phase 1: Enums, permissions, account_settings, audit_log,
|
||||
* GDPR register, file metadata, storage buckets.
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. New CMS Enums
|
||||
-- =====================================================
|
||||
|
||||
create type public.cms_field_type as enum(
|
||||
'text',
|
||||
'textarea',
|
||||
'richtext',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'hidden',
|
||||
'select',
|
||||
'password',
|
||||
'file',
|
||||
'date',
|
||||
'time',
|
||||
'decimal',
|
||||
'integer',
|
||||
'email',
|
||||
'phone',
|
||||
'url',
|
||||
'currency',
|
||||
'iban',
|
||||
'color',
|
||||
'computed'
|
||||
);
|
||||
|
||||
create type public.cms_module_status as enum(
|
||||
'active',
|
||||
'inactive',
|
||||
'archived'
|
||||
);
|
||||
|
||||
create type public.cms_record_status as enum(
|
||||
'active',
|
||||
'locked',
|
||||
'deleted',
|
||||
'archived'
|
||||
);
|
||||
|
||||
create type public.gdpr_legal_basis as enum(
|
||||
'consent',
|
||||
'contract',
|
||||
'legal_obligation',
|
||||
'vital_interest',
|
||||
'public_interest',
|
||||
'legitimate_interest'
|
||||
);
|
||||
|
||||
create type public.cms_account_type as enum(
|
||||
'verein',
|
||||
'vhs',
|
||||
'hotel',
|
||||
'kommune',
|
||||
'generic'
|
||||
);
|
||||
|
||||
create type public.audit_action as enum(
|
||||
'insert',
|
||||
'update',
|
||||
'delete',
|
||||
'lock'
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. Extend app_permissions with CMS values
|
||||
-- =====================================================
|
||||
-- ALTER TYPE … ADD VALUE cannot run inside a transaction in
|
||||
-- older Postgres, but Supabase migrations run outside
|
||||
-- explicit transactions by default.
|
||||
|
||||
alter type public.app_permissions add value if not exists 'modules.read';
|
||||
alter type public.app_permissions add value if not exists 'modules.write';
|
||||
alter type public.app_permissions add value if not exists 'modules.delete';
|
||||
alter type public.app_permissions add value if not exists 'modules.insert';
|
||||
alter type public.app_permissions add value if not exists 'modules.lock';
|
||||
alter type public.app_permissions add value if not exists 'modules.import';
|
||||
alter type public.app_permissions add value if not exists 'modules.export';
|
||||
alter type public.app_permissions add value if not exists 'modules.print';
|
||||
alter type public.app_permissions add value if not exists 'modules.manage';
|
||||
alter type public.app_permissions add value if not exists 'members.read';
|
||||
alter type public.app_permissions add value if not exists 'members.write';
|
||||
alter type public.app_permissions add value if not exists 'courses.read';
|
||||
alter type public.app_permissions add value if not exists 'courses.write';
|
||||
alter type public.app_permissions add value if not exists 'bookings.read';
|
||||
alter type public.app_permissions add value if not exists 'bookings.write';
|
||||
alter type public.app_permissions add value if not exists 'finance.read';
|
||||
alter type public.app_permissions add value if not exists 'finance.write';
|
||||
alter type public.app_permissions add value if not exists 'finance.sepa';
|
||||
alter type public.app_permissions add value if not exists 'documents.generate';
|
||||
alter type public.app_permissions add value if not exists 'newsletter.send';
|
||||
|
||||
-- =====================================================
|
||||
-- 3. account_settings — extends accounts per tenant
|
||||
-- =====================================================
|
||||
create table if not exists public.account_settings (
|
||||
account_id uuid primary key references public.accounts(id) on delete cascade,
|
||||
|
||||
-- Organisation type
|
||||
account_type public.cms_account_type not null default 'generic',
|
||||
|
||||
-- Org identity
|
||||
org_name text,
|
||||
org_address text,
|
||||
org_postal_code text,
|
||||
org_city text,
|
||||
org_phone text,
|
||||
org_email text,
|
||||
org_website text,
|
||||
org_chairman text,
|
||||
|
||||
-- Branding
|
||||
logo_url text,
|
||||
primary_color text default '#0f172a',
|
||||
secondary_color text default '#3b82f6',
|
||||
|
||||
-- SEPA banking
|
||||
creditor_id text, -- Gläubiger-ID
|
||||
iban text,
|
||||
bic text,
|
||||
|
||||
-- Email
|
||||
email_sender_name text,
|
||||
email_footer text,
|
||||
|
||||
-- Feature flags per tenant (jsonb for flexibility)
|
||||
features jsonb not null default '{}'::jsonb,
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.account_settings is 'CMS-specific settings per team account (organisation, branding, SEPA, features)';
|
||||
|
||||
-- RLS
|
||||
alter table public.account_settings enable row level security;
|
||||
|
||||
-- Revoke + grant
|
||||
revoke all on public.account_settings from authenticated, service_role;
|
||||
grant select, insert, update on public.account_settings to authenticated;
|
||||
grant all on public.account_settings to service_role;
|
||||
|
||||
-- SELECT: any member of the account
|
||||
create policy account_settings_select
|
||||
on public.account_settings for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
-- INSERT: settings.manage permission
|
||||
create policy account_settings_insert
|
||||
on public.account_settings for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- UPDATE: settings.manage permission
|
||||
create policy account_settings_update
|
||||
on public.account_settings for update
|
||||
to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- Auto-update updated_at
|
||||
create or replace function public.update_account_settings_timestamp()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger trg_account_settings_updated_at
|
||||
before update on public.account_settings
|
||||
for each row
|
||||
execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 4. audit_log — immutable action log
|
||||
-- =====================================================
|
||||
create table if not exists public.audit_log (
|
||||
id bigint generated always as identity primary key,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
user_id uuid references auth.users(id) on delete set null,
|
||||
table_name text not null,
|
||||
record_id text not null,
|
||||
action public.audit_action not null,
|
||||
old_data jsonb,
|
||||
new_data jsonb,
|
||||
ip_address inet,
|
||||
user_agent text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.audit_log is 'Immutable audit trail for CMS data changes';
|
||||
|
||||
-- Indexes
|
||||
create index ix_audit_log_account on public.audit_log(account_id);
|
||||
create index ix_audit_log_table_record on public.audit_log(table_name, record_id);
|
||||
create index ix_audit_log_created on public.audit_log(created_at desc);
|
||||
create index ix_audit_log_user on public.audit_log(user_id);
|
||||
|
||||
-- RLS
|
||||
alter table public.audit_log enable row level security;
|
||||
|
||||
revoke all on public.audit_log from authenticated, service_role;
|
||||
grant select, insert on public.audit_log to authenticated;
|
||||
grant all on public.audit_log to service_role;
|
||||
|
||||
-- SELECT: members of the account
|
||||
create policy audit_log_select
|
||||
on public.audit_log for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
-- INSERT: any authenticated member of the account
|
||||
create policy audit_log_insert
|
||||
on public.audit_log for insert
|
||||
to authenticated
|
||||
with check (public.has_role_on_account(account_id));
|
||||
|
||||
-- No UPDATE/DELETE — audit log is append-only
|
||||
|
||||
-- =====================================================
|
||||
-- 5. gdpr_processing_register
|
||||
-- =====================================================
|
||||
create table if not exists public.gdpr_processing_register (
|
||||
id bigint generated always as identity primary key,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
purpose text not null,
|
||||
legal_basis public.gdpr_legal_basis not null,
|
||||
data_categories text not null,
|
||||
data_subjects text not null,
|
||||
recipients text,
|
||||
retention_period text not null,
|
||||
technical_measures text,
|
||||
responsible_person text,
|
||||
notes text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.gdpr_processing_register is 'GDPR Art. 30 processing activity register per account';
|
||||
|
||||
-- Index
|
||||
create index ix_gdpr_register_account on public.gdpr_processing_register(account_id);
|
||||
|
||||
-- RLS
|
||||
alter table public.gdpr_processing_register enable row level security;
|
||||
|
||||
revoke all on public.gdpr_processing_register from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.gdpr_processing_register to authenticated;
|
||||
grant all on public.gdpr_processing_register to service_role;
|
||||
|
||||
create policy gdpr_register_select
|
||||
on public.gdpr_processing_register for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy gdpr_register_insert
|
||||
on public.gdpr_processing_register for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy gdpr_register_update
|
||||
on public.gdpr_processing_register for update
|
||||
to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy gdpr_register_delete
|
||||
on public.gdpr_processing_register for delete
|
||||
to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- Auto-update updated_at
|
||||
create trigger trg_gdpr_register_updated_at
|
||||
before update on public.gdpr_processing_register
|
||||
for each row
|
||||
execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 6. cms_files — file metadata with Storage paths
|
||||
-- =====================================================
|
||||
create table if not exists public.cms_files (
|
||||
id bigint generated always as identity primary key,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
module_name text not null,
|
||||
record_id text not null,
|
||||
field_name text not null,
|
||||
file_name text not null,
|
||||
original_name text not null,
|
||||
mime_type text not null,
|
||||
file_size bigint not null default 0,
|
||||
storage_path text not null,
|
||||
created_at timestamptz not null default now(),
|
||||
created_by uuid references auth.users(id) on delete set null
|
||||
);
|
||||
|
||||
comment on table public.cms_files is 'File metadata linking CMS records to Supabase Storage objects';
|
||||
|
||||
-- Indexes
|
||||
create index ix_cms_files_account on public.cms_files(account_id);
|
||||
create index ix_cms_files_record on public.cms_files(module_name, record_id);
|
||||
create index ix_cms_files_field on public.cms_files(module_name, record_id, field_name);
|
||||
|
||||
-- RLS
|
||||
alter table public.cms_files enable row level security;
|
||||
|
||||
revoke all on public.cms_files from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.cms_files to authenticated;
|
||||
grant all on public.cms_files to service_role;
|
||||
|
||||
create policy cms_files_select
|
||||
on public.cms_files for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy cms_files_insert
|
||||
on public.cms_files for insert
|
||||
to authenticated
|
||||
with check (public.has_role_on_account(account_id));
|
||||
|
||||
create policy cms_files_update
|
||||
on public.cms_files for update
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy cms_files_delete
|
||||
on public.cms_files for delete
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
-- =====================================================
|
||||
-- 7. Storage Buckets
|
||||
-- =====================================================
|
||||
|
||||
-- CMS uploads (module file fields)
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('cms-uploads', 'cms-uploads', false)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- CMS documents (generated PDFs, invoices)
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('cms-documents', 'cms-documents', false)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- CMS exports (CSV, Excel exports)
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('cms-exports', 'cms-exports', false)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- Storage RLS: account members can access their own files
|
||||
-- Uses the first path segment as account_id
|
||||
create or replace function kit.get_storage_path_account_id(name text)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
-- Path format: {account_id}/...
|
||||
return split_part(name, '/', 1)::uuid;
|
||||
exception when others then
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function kit.get_storage_path_account_id(text)
|
||||
to authenticated, service_role;
|
||||
|
||||
-- RLS for cms-uploads bucket
|
||||
create policy cms_uploads_select
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-uploads'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_uploads_insert
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'cms-uploads'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_uploads_delete
|
||||
on storage.objects for delete
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-uploads'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
-- RLS for cms-documents bucket
|
||||
create policy cms_documents_select
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-documents'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_documents_insert
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'cms-documents'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
-- RLS for cms-exports bucket
|
||||
create policy cms_exports_select
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-exports'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_exports_insert
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'cms-exports'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user