-- Plesk Agency Portal MVP schema -- Run this in Supabase SQL editor. create extension if not exists pgcrypto; create table if not exists agencies ( id uuid primary key default gen_random_uuid(), name text not null, created_at timestamptz not null default now() ); create table if not exists agency_members ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, user_id uuid not null, role text not null check (role in ('owner', 'admin', 'member')), created_at timestamptz not null default now(), unique (agency_id, user_id) ); create table if not exists clients ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, name text not null, email text, phone text, notes text, created_at timestamptz not null default now() ); create table if not exists plesk_instances ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, name text not null, base_url text not null, auth_type text not null check (auth_type in ('api_key', 'basic', 'oauth_token')), auth_secret_encrypted text not null, status text not null default 'active' check (status in ('active', 'disabled')), last_connected_at timestamptz, created_at timestamptz not null default now() ); create table if not exists plesk_subscriptions ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, plesk_instance_id uuid not null references plesk_instances(id) on delete cascade, client_id uuid references clients(id) on delete set null, plesk_subscription_id text not null, name text, status text, owner_login text, plan_name text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (plesk_instance_id, plesk_subscription_id) ); create table if not exists plesk_domains ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, plesk_instance_id uuid not null references plesk_instances(id) on delete cascade, subscription_id uuid not null references plesk_subscriptions(id) on delete cascade, plesk_domain_id text not null, domain_name text not null, status text, created_at timestamptz not null default now(), unique (plesk_instance_id, plesk_domain_id), unique (plesk_instance_id, domain_name) ); create table if not exists actions_log ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, actor_user_id uuid, target_type text not null, target_id uuid, action text not null, status text not null default 'pending' check (status in ('pending', 'success', 'failed')), error_message text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create table if not exists billing_accounts ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, stripe_customer_id text, stripe_subscription_id text, plan_id text, subscription_status text, current_period_end timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (agency_id), unique (stripe_customer_id) ); create table if not exists entitlements ( id uuid primary key default gen_random_uuid(), agency_id uuid not null references agencies(id) on delete cascade, key text not null, value jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), unique (agency_id, key) ); create or replace function set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; drop trigger if exists trg_plesk_subscriptions_updated_at on plesk_subscriptions; create trigger trg_plesk_subscriptions_updated_at before update on plesk_subscriptions for each row execute function set_updated_at(); drop trigger if exists trg_billing_accounts_updated_at on billing_accounts; create trigger trg_billing_accounts_updated_at before update on billing_accounts for each row execute function set_updated_at(); create or replace function is_agency_member(target_agency_id uuid) returns boolean language sql stable security definer set search_path = public as $$ select exists ( select 1 from agency_members am where am.agency_id = target_agency_id and am.user_id = auth.uid() ); $$; create or replace function is_agency_admin(target_agency_id uuid) returns boolean language sql stable security definer set search_path = public as $$ select exists ( select 1 from agency_members am where am.agency_id = target_agency_id and am.user_id = auth.uid() and am.role in ('owner', 'admin') ); $$; alter table agencies enable row level security; alter table agency_members enable row level security; alter table clients enable row level security; alter table plesk_instances enable row level security; alter table plesk_subscriptions enable row level security; alter table plesk_domains enable row level security; alter table actions_log enable row level security; alter table billing_accounts enable row level security; alter table entitlements enable row level security; drop policy if exists agencies_select on agencies; create policy agencies_select on agencies for select using (is_agency_member(id)); drop policy if exists agencies_update on agencies; create policy agencies_update on agencies for update using (is_agency_admin(id)); drop policy if exists agency_members_select on agency_members; create policy agency_members_select on agency_members for select using (is_agency_member(agency_id)); drop policy if exists agency_members_manage on agency_members; create policy agency_members_manage on agency_members for all using (is_agency_admin(agency_id)) with check (is_agency_admin(agency_id)); drop policy if exists clients_member_access on clients; create policy clients_member_access on clients for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id)); drop policy if exists plesk_instances_member_access on plesk_instances; create policy plesk_instances_member_access on plesk_instances for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id)); drop policy if exists plesk_subscriptions_member_access on plesk_subscriptions; create policy plesk_subscriptions_member_access on plesk_subscriptions for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id)); drop policy if exists plesk_domains_member_access on plesk_domains; create policy plesk_domains_member_access on plesk_domains for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id)); drop policy if exists actions_log_member_access on actions_log; create policy actions_log_member_access on actions_log for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id)); drop policy if exists billing_accounts_member_access on billing_accounts; create policy billing_accounts_member_access on billing_accounts for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id)); drop policy if exists entitlements_member_access on entitlements; create policy entitlements_member_access on entitlements for all using (is_agency_member(agency_id)) with check (is_agency_member(agency_id));