449 lines
14 KiB
PL/PgSQL
449 lines
14 KiB
PL/PgSQL
/*
|
|
* -------------------------------------------------------
|
|
* 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))
|
|
);
|
|
|