From 352e22a64b3f6ee73c81134254a3b89b88cd78c4 Mon Sep 17 00:00:00 2001 From: bes-r <27369668+bes-r@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:26:01 +0200 Subject: [PATCH] v0.2.00: schoolsysteem met rollen (super/admin/groepsleiding/leerling), koppelcodes, server-API en beheerpaneel --- .env.example | 5 + compose.yaml | 2 + db/002_schools_auth.sql | 52 +++++++ deploy/compose.deploy.yaml | 2 + public/css/teach.css | 56 ++++++++ public/index.html | 20 ++- public/js/admin.js | 223 ++++++++++++++++++++++++++++++ public/js/app.js | 23 ++-- public/js/core.js | 232 +++++++++++++++++++------------ src/api.js | 271 +++++++++++++++++++++++++++++++++++++ src/auth.js | 82 +++++++++++ src/server.js | 15 +- 12 files changed, 874 insertions(+), 109 deletions(-) create mode 100644 db/002_schools_auth.sql create mode 100644 public/js/admin.js create mode 100644 src/api.js create mode 100644 src/auth.js diff --git a/.env.example b/.env.example index e657648..ab8591b 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,8 @@ POSTGRES_PASSWORD=change-me-locally # Connectiestring die de app gebruikt. # Hostnaam 'db' verwijst naar de postgres-service binnen het compose-netwerk. DATABASE_URL=postgres://teach:change-me-locally@db:5432/teach + +# Eerste overkoepelende beheerder (wordt eenmalig aangemaakt als er nog geen bestaat). +# Laat SUPER_PASS leeg om een willekeurig wachtwoord in de serverlog te krijgen. +SUPER_USER=beheerder +SUPER_PASS= diff --git a/compose.yaml b/compose.yaml index f1f33d9..4881ba6 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,6 +16,8 @@ services: PORT: 3000 DATABASE_URL: ${DATABASE_URL} APP_VERSION: dev + SUPER_USER: ${SUPER_USER:-beheerder} + SUPER_PASS: ${SUPER_PASS:-} ports: - "3000:3000" restart: unless-stopped diff --git a/db/002_schools_auth.sql b/db/002_schools_auth.sql new file mode 100644 index 0000000..05bb747 --- /dev/null +++ b/db/002_schools_auth.sql @@ -0,0 +1,52 @@ +-- v0.2.00: scholen, klassen, rollen en sessies. +-- Rollen: super (overkoepelend beheerder), admin (schoolbeheerder), +-- teacher (groepsleiding), pupil (leerling). + +CREATE TABLE IF NOT EXISTS schools ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS classes ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE, + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +ALTER TABLE users + ADD COLUMN IF NOT EXISTS school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE, + ADD COLUMN IF NOT EXISTS display_name TEXT, + ADD COLUMN IF NOT EXISTS password_hash TEXT, + -- alleen voor leerlingen: wachtwoord dat groepsleiding/beheer mag inzien + ADD COLUMN IF NOT EXISTS password_plain TEXT, + -- eenmalige koppelcode waarmee nieuwe gebruikers hun account activeren + ADD COLUMN IF NOT EXISTS link_code TEXT UNIQUE, + ADD COLUMN IF NOT EXISTS class_id BIGINT REFERENCES classes(id) ON DELETE SET NULL, + -- alle app-gegevens van de gebruiker (borden, thema's, eigen woorden) + ADD COLUMN IF NOT EXISTS data JSONB NOT NULL DEFAULT '{}'::jsonb; + +-- gebruikersnaam uniek binnen een school; supers (school_id IS NULL) landelijk uniek +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_username_key; +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_school_username + ON users (school_id, lower(username)) WHERE school_id IS NOT NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_super_username + ON users (lower(username)) WHERE school_id IS NULL; + +-- welke groepsleiding hoort bij welke klas +CREATE TABLE IF NOT EXISTS class_teachers ( + class_id BIGINT NOT NULL REFERENCES classes(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (class_id, user_id) +); + +CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_users_school ON users(school_id); +CREATE INDEX IF NOT EXISTS idx_users_class ON users(class_id); diff --git a/deploy/compose.deploy.yaml b/deploy/compose.deploy.yaml index 5520b4e..c92f55a 100644 --- a/deploy/compose.deploy.yaml +++ b/deploy/compose.deploy.yaml @@ -20,6 +20,8 @@ services: PORT: 3000 DATABASE_URL: ${DATABASE_URL} APP_VERSION: ${IMAGE} + SUPER_USER: ${SUPER_USER:-beheerder} + SUPER_PASS: ${SUPER_PASS:-} ports: # Alleen op localhost van de VM; nginx zit ervoor als reverse proxy - "127.0.0.1:${APP_PORT:-3000}:3000" diff --git a/public/css/teach.css b/public/css/teach.css index f54678c..575f636 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -944,3 +944,59 @@ .thint{font-size:11.5px; color:var(--muted); text-align:center; line-height:1.4;} .timer.flash{animation:tflash .5s 4;} @keyframes tflash{50%{background:#fdeaa0;}} + +/* ---------- login-extra's (school, koppelcode) ---------- */ +#liSchool{ + width:100%; border:2px solid #cdd9e8; border-radius:12px; padding:11px 12px; + font:inherit; font-size:15px; outline:none; margin-bottom:10px; background:#fff; +} +.lidiv{height:1.5px; background:#e3eaf3; margin:14px 0;} +.lisub{font-size:13px; font-weight:700; color:var(--muted); margin-bottom:8px;} +#ownPwRow{margin-bottom:10px;} +#ownPwRow input{ + flex:1; min-width:0; border:2px solid #cdd9e8; border-radius:12px; + padding:10px 12px; font:inherit; font-size:15px; outline:none; +} +/* ---------- beheerpaneel ---------- */ +#adminModal{width:min(760px,94vw); max-height:86vh; overflow:auto;} +.am-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;} +.am-head h2{margin:0; font-size:20px;} +.am-h3{margin:14px 0 6px; font-size:14px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px;} +.am-msg{min-height:20px; font-size:13.5px; font-weight:800; color:var(--green); margin:4px 0;} +.am-add{display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:8px 0;} +.am-inp,.am-sel{ + border:2px solid #cdd9e8; border-radius:10px; padding:8px 10px; font:inherit; + font-size:14px; outline:none; background:#fff; min-width:0; +} +.am-inp{flex:1; min-width:140px;} +.am-list{display:flex; flex-direction:column; gap:5px;} +.am-group{font-weight:900; font-size:13px; margin-top:10px; color:var(--ink);} +.am-row{ + display:flex; gap:8px; align-items:center; background:#f7fafd; + border-radius:10px; padding:6px 10px; flex-wrap:wrap; +} +.am-name{font-weight:800; font-size:14px; flex:1; min-width:120px;} +.am-role{font-size:12px; font-weight:700; color:var(--muted);} +.am-pw{ + border:2px solid #cdd9e8; border-radius:8px; padding:5px 8px; font:inherit; + font-size:13px; width:110px; outline:none; +} +.am-code{font-size:12.5px; font-weight:800; color:var(--orange);} +.am-btn{ + border:none; background:#e8eef6; border-radius:8px; padding:6px 10px; + font:inherit; font-weight:800; font-size:12px; cursor:pointer; color:var(--ink); +} +.am-btn:hover{background:#dbe5f1;} +.am-del{ + border:none; background:#fbe3e1; border-radius:8px; width:28px; height:28px; + cursor:pointer; font-size:13px; +} +.am-del:hover{background:var(--red);} +.am-classes{display:flex; gap:6px; flex-wrap:wrap;} +.am-chip{ + display:inline-flex; gap:6px; align-items:center; background:#fff; + border:2px solid #e3eaf3; border-radius:999px; padding:5px 12px; + font-weight:800; font-size:13px; +} +.am-chipx{border:none; background:transparent; color:var(--red); font-weight:900; cursor:pointer; padding:0; font-size:11px;} +body.hc .am-row,body.hc .am-chip{border:1.5px solid #000; background:#fff;} diff --git a/public/index.html b/public/index.html index 54028b4..773456a 100644 --- a/public/index.html +++ b/public/index.html @@ -24,6 +24,7 @@ +