From d325574a7dc08af5c116243d58c401efdfcbaa76 Mon Sep 17 00:00:00 2001 From: Jesper Pedersen Date: Mon, 8 Jun 2026 14:34:10 +0200 Subject: [PATCH] feat: restrict AI Bibliotek sign-up to whitelisted municipalities The library is intended only for employees of participating Danish municipalities. Sign-up now uses a select of whitelisted myndigheder and enforces an email-domain match, so the access rule is tangible in the mock. The fuller model (email verification, per-domain admin) is documented as design intent since the localStorage prototype does not implement it. Co-authored-by: Claude --- CHANGELOG.md | 4 ++++ docs/projects/ai-bibliotek/index.md | 14 +++++++++++++ .../projects/ai-bibliotek/mocks/js/auth.js | 21 +++++++++++++++++++ .../ai-bibliotek/mocks/js/views/login.js | 13 +++++++++--- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33868e5..1ec1bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Changed — Whitelisted municipality sign-up (ai-bibliotek) +- Sign-up's "Myndighed eller organisation" field is now a select limited to whitelisted municipalities, and registration enforces that the email domain matches the selected municipality's domain (shared `MUNICIPALITIES` list in `auth.js`) +- Documented the intended access model in `index.md`: whitelisted email domains, account verification via email link on sign-up, and a per-domain admin who can delete and promote users (these remain design intent — the `localStorage` mock does not implement them) + ### Changed — Real PDF links in Vosnæs hearing detail mock (deltag-aarhus) - The four materials in the Vosnæs prototype (`mocks/index.html`) now link to the actual public PDFs (Forslag til Lokalplan nr. 1237, Miljøvurderingsrapport, Ikke-teknisk resumé, Forslag til §25-tilladelse), opening in a new tab. The lokalplan preview modal's "Åbn på ny side" and "Download PDF" buttons point at the live `plandata.dk` document. diff --git a/docs/projects/ai-bibliotek/index.md b/docs/projects/ai-bibliotek/index.md index e1f2132..1bdc2b2 100644 --- a/docs/projects/ai-bibliotek/index.md +++ b/docs/projects/ai-bibliotek/index.md @@ -35,6 +35,16 @@ Hero med søgefelt, kort introduktion og statistik (antal assistenter, deltagend Simpel brugerflade hvor en medarbejder kan oprette en konto (i første omgang opret sig selv) eller logge ind. Brugere gemmes i `localStorage`. Ingen reel auth — kun til demoformål. +I prototypen vælges **myndighed** fra en dropdown (kun whitelistede myndigheder), og registrering kræver, at e-mailen ligger på den valgte myndigheds domæne. Det illustrerer den tiltænkte adgangsmodel: + +- **Whitelistet domæne** — kun medarbejdere med en arbejdsmail på et whitelistet domæne (fx `@aarhus.dk`) kan oprette sig. Domænet er adgangsporten; nye myndigheder optages ved at få deres domæne tilføjet til listen. +- **Verifikation ved oprettelse** — den nye bruger bekræfter sin konto via et link sendt til e-mailen, før kontoen aktiveres. +- **Domæne-admin** — for hvert domæne udpeges en bruger som **admin** med rettigheder til at **slette** brugere og **forfremme** andre brugere (herunder til admin) inden for samme domæne. + +::: info Prototype +Selve verifikationsflowet, admin-rollen og brugeradministrationen er kun beskrevet som tiltænkt model — prototypen simulerer dem ikke. Mocken håndhæver dog whitelisten og domæne-matchet ved oprettelse. +::: + ### Katalog og søgning Fritekstsøgning kombineret med facetter: **oprindelseskommune, sprogmodel, rammeværk** (i dag OpenWebUI) og **datafølsomhed** (almindelige personoplysninger / fortrolige / personfølsomme). Resultater vises som kort med badges. @@ -82,6 +92,10 @@ Prototypen er et diskussionsgrundlag, ikke en implementeringsklar løsning. En r I prototypen er **kataloget offentligt at browse**, mens det at **dele og eksportere** kræver login. Det er en antagelse, ikke en beslutning. Skal selve eksistensen af en assistent (navn, formål, kommune) være åben, mens JSON og modelkort er bag login? Eller skal hele biblioteket være lukket for ikke-myndigheder? +### Hvem styrer whitelisten og admin-rollen? + +Adgang gives via whitelistede e-mail-domæner. Hvem godkender, at et nyt domæne (en ny myndighed) optages? Og hvem udpeger den første admin for et domæne, før der overhovedet er en admin til at forfremme andre? Der skal tages stilling til både onboarding af nye myndigheder og den indledende rolletildeling. + ### Hvad er der i JSON-filerne? Kan vi ukritisk dele indholdet af en OWUI-assistents JSON? Systemprompter kan indeholde interne formuleringer, henvisninger til konkrete sager eller forudsætninger, der ikke bør deles bredt. Der skal tages stilling til, hvad der reviewes inden deling. diff --git a/docs/public/projects/ai-bibliotek/mocks/js/auth.js b/docs/public/projects/ai-bibliotek/mocks/js/auth.js index cac7a1e..388b366 100644 --- a/docs/public/projects/ai-bibliotek/mocks/js/auth.js +++ b/docs/public/projects/ai-bibliotek/mocks/js/auth.js @@ -18,6 +18,19 @@ export const DEMO_USERS = [ { id: "demo-odense", name: "Jonas Holm", organization: "Odense Kommune", email: "jonas@odense.dk", password: "demo1234" } ]; +/* Whitelisted myndigheder. Only employees with an email on a municipality's + domain may register — this list drives both the sign-up select and the + domain check in register(). In a real system the whitelist is the gate; + here it just makes the rule tangible in the prototype. */ +export const MUNICIPALITIES = [ + { name: "Aarhus Kommune", domain: "aarhus.dk" }, + { name: "Odense Kommune", domain: "odense.dk" }, + { name: "Københavns Kommune", domain: "kk.dk" }, + { name: "Aalborg Kommune", domain: "aalborg.dk" }, + { name: "Vejle Kommune", domain: "vejle.dk" }, + { name: "Randers Kommune", domain: "randers.dk" } +]; + export const auth = { currentUser() { const session = store.getSession(); @@ -33,6 +46,14 @@ export const auth = { if (password.length < 4) { throw new Error("Adgangskoden skal være mindst 4 tegn."); } + const muni = MUNICIPALITIES.find(m => m.name === organization?.trim()); + if (!muni) { + throw new Error("Vælg en myndighed fra listen."); + } + const domain = email.split("@")[1] || ""; + if (domain !== muni.domain) { + throw new Error(`E-mailen skal være en @${muni.domain}-adresse for ${muni.name}.`); + } const users = store.getUsers(); if (users.some(u => u.email === email)) { throw new Error("En bruger med denne e-mail findes allerede."); diff --git a/docs/public/projects/ai-bibliotek/mocks/js/views/login.js b/docs/public/projects/ai-bibliotek/mocks/js/views/login.js index 93a3c69..6672f01 100644 --- a/docs/public/projects/ai-bibliotek/mocks/js/views/login.js +++ b/docs/public/projects/ai-bibliotek/mocks/js/views/login.js @@ -1,5 +1,5 @@ import { el, clear, navigate, toast } from "../util.js"; -import { auth, DEMO_USERS } from "../auth.js"; +import { auth, DEMO_USERS, MUNICIPALITIES } from "../auth.js"; export function render(root, query = {}) { clear(root); @@ -106,11 +106,18 @@ function registerForm() { ]), el("label", { class: "field" }, [ "Myndighed eller organisation", - el("input", { type: "text", name: "organization", placeholder: "f.eks. Aarhus Kommune", autocomplete: "organization" }) + el("select", { name: "organization", required: true }, [ + el("option", { value: "", disabled: true, selected: true }, "Vælg myndighed …"), + ...MUNICIPALITIES.map(m => + el("option", { value: m.name }, `${m.name} (${m.domain})`) + ) + ]), + el("span", { class: "hint" }, "Kun whitelistede myndigheder. Kontakt os for at få din myndighed tilføjet.") ]), el("label", { class: "field" }, [ "E-mail", - el("input", { type: "email", name: "email", required: true, autocomplete: "email" }) + el("input", { type: "email", name: "email", required: true, autocomplete: "email" }), + el("span", { class: "hint" }, "Skal være din arbejdsmail på myndighedens domæne (f.eks. @aarhus.dk).") ]), el("label", { class: "field" }, [ "Adgangskode",