since3-portal/lib/supabase/migrations/20261011100000_internal_credentials.sql
2026-10-11 18:22:32 +02:00

101 lines
5.9 KiB
PL/PgSQL

-- Interne Zugänge von Since3 (eigene Hosting-, Tool- und Domain-Accounts). Nur für Admins.
-- Gleiches Prinzip wie bei den Kunden-Passwörtern (credentials): In der Tabelle steht nie das Passwort,
-- nur die Vault-Referenz. Schreiben und Klartext nur über die Funktionen unten, jeder Abruf wird protokolliert.
create table public.internal_credentials (
id uuid primary key default gen_random_uuid(),
service text not null, -- Schlüssel aus SERVICES (services-data.ts)
title text not null default '', -- z. B. "Since3 Team"; leer = Name des Dienstes
username text not null,
url text not null default '', -- Login-Adresse (optional)
plan text not null default 'free', -- 'free' | 'paid'
price numeric(10,2), -- Euro, nur bei 'paid'
billing_cycle text, -- 'monthly' | 'yearly' | 'once', nur bei 'paid'
vault_secret_id uuid not null, -- -> vault.secrets
created_by uuid references public.profiles(id) on delete set null default auth.uid(),
updated_by uuid references public.profiles(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
-- Kostenlos = weder Preis noch Abrechnung; bezahlt = beides
constraint internal_credentials_cost_check check (
(plan = 'free' and price is null and billing_cycle is null)
or (plan = 'paid' and price is not null and price >= 0 and billing_cycle in ('monthly', 'yearly', 'once'))
)
);
create trigger set_updated_at before update on public.internal_credentials
for each row execute function public.set_updated_at();
-- Vault-Eintrag beim Löschen mit aufräumen (gleiche Funktion wie bei credentials)
create trigger internal_credentials_delete_vault_secret
after delete on public.internal_credentials
for each row execute function public.delete_vault_secret();
-- Lesen (ohne Passwort) und Löschen nur Admins; Anlegen/Ändern nur über die Funktionen
alter table public.internal_credentials enable row level security;
create policy admin_select on public.internal_credentials for select to authenticated
using ((select public.is_admin()));
create policy admin_delete on public.internal_credentials for delete to authenticated
using ((select public.is_admin()));
create or replace function public.create_internal_credential(
p_service text, p_title text, p_username text, p_url text,
p_plan text, p_price numeric, p_billing_cycle text, p_password text
) returns uuid language plpgsql security definer set search_path = '' as $$
declare v_secret uuid; v_id uuid;
begin
if not public.is_admin() then raise exception 'forbidden' using errcode = '42501'; end if;
v_secret := vault.create_secret(p_password, 'internal_' || gen_random_uuid()::text, p_service);
insert into public.internal_credentials
(service, title, username, url, plan, price, billing_cycle, vault_secret_id, updated_by)
values
(p_service, coalesce(p_title, ''), p_username, coalesce(p_url, ''), p_plan,
case when p_plan = 'paid' then p_price end,
case when p_plan = 'paid' then p_billing_cycle end,
v_secret, auth.uid())
returning id into v_id;
return v_id;
end $$;
-- p_password = NULL oder leer -> Passwort bleibt unverändert
create or replace function public.update_internal_credential(
p_id uuid, p_service text, p_title text, p_username text, p_url text,
p_plan text, p_price numeric, p_billing_cycle text, p_password text default null
) returns void language plpgsql security definer set search_path = '' as $$
declare v_secret uuid;
begin
if not public.is_admin() then raise exception 'forbidden' using errcode = '42501'; end if;
update public.internal_credentials
set service = p_service, title = coalesce(p_title, ''), username = p_username,
url = coalesce(p_url, ''), plan = p_plan,
price = case when p_plan = 'paid' then p_price end,
billing_cycle = case when p_plan = 'paid' then p_billing_cycle end,
updated_by = auth.uid()
where id = p_id
returning vault_secret_id into v_secret;
if not found then raise exception 'internal credential not found'; end if;
if p_password is not null and p_password <> '' then
perform vault.update_secret(v_secret, p_password);
end if;
end $$;
-- Klartext nur für Admins; landet im selben Zugriffsprotokoll wie die Kunden-Passwörter
create or replace function public.reveal_internal_credential(p_id uuid)
returns text language plpgsql security definer set search_path = '' as $$
declare v_secret uuid; v_pw text;
begin
if not public.is_admin() then raise exception 'forbidden' using errcode = '42501'; end if;
select vault_secret_id into v_secret from public.internal_credentials where id = p_id;
if not found then raise exception 'internal credential not found'; end if;
select decrypted_secret into v_pw from vault.decrypted_secrets where id = v_secret;
insert into public.credential_access_log (credential_id, user_id, action)
values (p_id, auth.uid(), 'reveal_internal');
return v_pw;
end $$;
revoke execute on function public.create_internal_credential(text, text, text, text, text, numeric, text, text) from public, anon;
revoke execute on function public.update_internal_credential(uuid, text, text, text, text, text, numeric, text, text) from public, anon;
revoke execute on function public.reveal_internal_credential(uuid) from public, anon;
grant execute on function public.create_internal_credential(text, text, text, text, text, numeric, text, text) to authenticated;
grant execute on function public.update_internal_credential(uuid, text, text, text, text, text, numeric, text, text) to authenticated;
grant execute on function public.reveal_internal_credential(uuid) to authenticated;