-- Guidad Felsökning: schema för den självhostade plattformen. -- Multi-tenant: varje organisation är en egen tenant — all ärendedata är -- organisationsknuten och API:t släpper aldrig data över organisationsgränsen. -- Append-only garanteras med triggers: historik kan inte ändras eller -- raderas oavsett vilken roll som ansluter. create extension if not exists pgcrypto; create table if not exists organisationer ( id uuid primary key default gen_random_uuid(), namn text not null, -- Vad som visas när ett ärende startas (objekttyper, -- identifieringsmetoder) — sätts av systemadministratören. installningar jsonb not null default '{}'::jsonb, skapad timestamptz not null default now() ); alter table organisationer add column if not exists installningar jsonb not null default '{}'::jsonb; -- Märkesspecifika kopplingar per organisation. Uppgifterna lagras -- krypterade (AES-256-GCM, nyckel ur INTEGRATION_NYCKEL) och lämnar -- aldrig servern i klartext — klienten ser bara maskerade värden. create table if not exists integrationer ( organisation_id uuid not null references organisationer(id), leverantor text not null, uppgifter_krypt text not null, aktiv boolean not null default true, skapad timestamptz not null default now(), uppdaterad timestamptz not null default now(), senast_testad timestamptz, senaste_status text, primary key (organisation_id, leverantor) ); create table if not exists anvandare ( id uuid primary key default gen_random_uuid(), organisation_id uuid not null references organisationer(id), epost text unique not null, losen_hash text not null, namn text not null, roll text not null default 'tekniker' check (roll in ('tekniker', 'arbetsledare', 'admin')), -- En avaktiverad användare kan varken logga in eller använda en token -- som redan är utfärdad. aktiv boolean not null default true, -- Räknare för återkallelse. Utfärdade tokens bär sitt värde; höjs det -- slutar alla tidigare utfärdade att gälla omedelbart. Loggen rörs -- inte — historiken är fortfarande knuten till personen. token_version integer not null default 0, skapad timestamptz not null default now() ); alter table anvandare add column if not exists aktiv boolean not null default true; alter table anvandare add column if not exists token_version integer not null default 0; -- Inloggningsförsök: underlag för takt-begränsning som fungerar bakom -- flera repliker (den i minnet gör det inte). Inget lösenord lagras — -- bara att ett försök skedde och om det lyckades. create table if not exists inloggningsforsok ( id bigserial primary key, epost text not null, kalla text not null default '', lyckades boolean not null, tidpunkt timestamptz not null default now() ); create index if not exists inloggningsforsok_epost_idx on inloggningsforsok (epost, tidpunkt desc); create index if not exists inloggningsforsok_kalla_idx on inloggningsforsok (kalla, tidpunkt desc); create table if not exists felsokning_arenden ( id text primary key, organisation_id uuid not null references organisationer(id), nummer integer not null, skapad timestamptz not null, delningskod text unique, metodik_id text, skapad_av uuid references anvandare(id), insatt timestamptz not null default now() ); create index if not exists felsokning_arenden_org_idx on felsokning_arenden (organisation_id, skapad desc); -- Bilagor: foton, video och instrumentbilder. Innehållet ligger utanför -- händelsen; loggen bär en referens och innehållets SHA-256. Hashen står -- i den append-only-skyddade loggen, så en utbytt bild går att upptäcka — -- starkare bevisvärde än när bilden låg inbäddad och måste tros på. create table if not exists bilagor ( id text primary key, organisation_id uuid not null references organisationer(id), arende_id text not null, hash text not null, mediatyp text not null, storlek integer not null, laddad_av uuid references anvandare(id), skapad timestamptz not null default now() ); create index if not exists bilagor_arende_idx on bilagor (arende_id); create index if not exists bilagor_hash_idx on bilagor (hash); -- Själva bytesen när BILAGE_LAGE=databas. Innehållsadresserat: samma -- foto som dokumenteras två gånger lagras en gång. I s3-läget är den -- här tabellen tom och innehållet ligger i objektlagringen. create table if not exists bilage_innehall ( hash text primary key, data bytea not null, skapad timestamptz not null default now() ); create table if not exists felsokning_handelser ( id text primary key, arende_id text not null references felsokning_arenden(id), tidpunkt timestamptz not null, anvandare text not null, handelse jsonb not null, insatt timestamptz not null default now() ); create index if not exists felsokning_handelser_arende_idx on felsokning_handelser (arende_id, tidpunkt); create or replace function forbjud_andring() returns trigger language plpgsql as $$ begin raise exception 'Loggen är append-only — historik kan inte ändras eller raderas'; end $$; drop trigger if exists handelser_append_only on felsokning_handelser; create trigger handelser_append_only before update or delete on felsokning_handelser for each row execute function forbjud_andring(); drop trigger if exists arenden_append_only on felsokning_arenden; create trigger arenden_append_only before update or delete on felsokning_arenden for each row execute function forbjud_andring(); -- Live Share-delningar: återkallbara länkar med behörighetsnivå. -- (Åtkomststyrning, inte journal — därför ingen append-only-trigger: -- återkallelse sätter aterkallad-tidpunkten.) create table if not exists delningar ( kod text primary key, arende_id text not null references felsokning_arenden(id), niva text not null check (niva in ('kund', 'partner', 'intern')), skapad_av uuid references anvandare(id), skapad timestamptz not null default now(), aterkallad timestamptz ); create index if not exists delningar_arende_idx on delningar (arende_id);