From 7287e49e0c68352f46ff14f05b57e5fe88e86d56 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 15 Jul 2026 16:35:55 +0200 Subject: [PATCH] v0.3.38-beta: koppel ook schoolbeheerders en systeemmanagers aan klassen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - De klas-koppeling (chips + "voeg toe aan klas…") staat nu bij elke staf-rol, niet alleen bij groepsleiding; de klasnamen staan ook in de rijkop ("Schoolbeheerder · Groep 4") - Server: /admin/classes/:id/teachers accepteert alle staf-rollen; alleen leerlingen niet (die horen al bij één klas). Een systeemmanager is school-onafhankelijk en mag aan klassen van elke school; overige staf moet bij dezelfde school horen - Nieuwe servertest: admin en super koppelen ok, leerling en staf van een andere school geweigerd --- VERSION | 2 +- public/js/admin.js | 11 +++++------ public/js/core.js | 2 +- src/api.js | 13 ++++++++----- test/schools.test.js | 31 ++++++++++++++++++++++++++++++- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/VERSION b/VERSION index efed92e..ff16b66 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.37-beta +0.3.38-beta diff --git a/public/js/admin.js b/public/js/admin.js index 220fdc5..8c493ff 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -198,15 +198,13 @@ if(u.role==="pupil"){ const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId)); row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : ""))); - }else if(u.role==="teacher"){ + }else{ const names = (u.classIds||[]) .map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name) .filter(Boolean); row.appendChild(h("span","am-role", T(roleKey(u.role)) + (names.length ? ` · ${names.join(", ")}` : "") + (u.pending ? ` · ${T("amPending")}` : ""))); - }else{ - row.appendChild(h("span","am-role", T(roleKey(u.role)) + (u.pending ? ` · ${T("amPending")}` : ""))); } if(u.role!=="pupil" && can("users.role.change") && u.id!==currentUser.id){ /* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super); @@ -220,9 +218,10 @@ }); tools.appendChild(field("amFieldRole", rs)); } - /* groepsleiding eenvoudig aan klassen koppelen: huidige klassen als - chips met ✕, en een select om een klas toe te voegen */ - if(u.role==="teacher" && can("classes.manage") && CLASSES.length){ + /* elke staf-rol eenvoudig aan klassen koppelen (groepsleiding, + schoolbeheerder én systeemmanager): huidige klassen als chips met ✕, + en een select om een klas toe te voegen */ + if(u.role!=="pupil" && can("classes.manage") && CLASSES.length){ const wrap = h("div","am-classes"); (u.classIds||[]).forEach(cid=>{ const c = CLASSES.find(x=>Number(x.id)===Number(cid)); diff --git a/public/js/core.js b/public/js/core.js index e2bfa35..3637e6f 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.3.37-beta"; +const VERSION = "0.3.38-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/src/api.js b/src/api.js index 3f47840..474e67b 100644 --- a/src/api.js +++ b/src/api.js @@ -331,19 +331,22 @@ export default async function api(app) { return { ok: true }; }); // groepsleiding aan klas koppelen/loskoppelen + // Elke staf-rol (groepsleiding, schoolbeheerder, systeemmanager) kan aan + // een klas gekoppeld worden; alleen leerlingen niet (die horen al bij één + // klas via users.class_id). Een systeemmanager is school-onafhankelijk en + // mag dus aan klassen van elke school gekoppeld worden. app.post('/admin/classes/:id/teachers', async (req, reply) => { need(req, reply, PERMISSIONS['classes.manage']); const { userId, remove } = req.body ?? {}; if (!userId) return fail(reply, 400, 'gebruiker verplicht'); const c = (await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id])).rows[0]; - const u = (await pool.query(`SELECT u.*, EXISTS ( - SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher' - ) AS extra_teacher FROM users u WHERE u.id = $1`, [userId])).rows[0]; + const u = (await pool.query('SELECT * FROM users WHERE id = $1', [userId])).rows[0]; if (!c || !u) return fail(reply, 404, 'klas of gebruiker onbekend'); if (!req.user.allRoles.includes('super') && Number(c.school_id) !== Number(req.user.school_id)) return fail(reply, 403, 'geen rechten'); - if (Number(c.school_id) !== Number(u.school_id)) return fail(reply, 400, 'klas en groepsleiding moeten bij dezelfde school horen'); - if (u.role !== 'teacher' && !u.extra_teacher) return fail(reply, 400, 'gebruiker is geen groepsleiding'); + if (u.role === 'pupil') return fail(reply, 400, 'leerlingen horen al bij één klas'); + if (u.role !== 'super' && Number(c.school_id) !== Number(u.school_id)) + return fail(reply, 400, 'klas en gebruiker moeten bij dezelfde school horen'); if (remove) await pool.query('DELETE FROM class_teachers WHERE class_id = $1 AND user_id = $2', [c.id, u.id]); else await pool.query('INSERT INTO class_teachers (class_id, user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [c.id, u.id]); return { ok: true }; diff --git a/test/schools.test.js b/test/schools.test.js index bc62ba1..b761692 100644 --- a/test/schools.test.js +++ b/test/schools.test.js @@ -5,7 +5,7 @@ import cookie from '@fastify/cookie'; import rateLimit from '@fastify/rate-limit'; import api from '../src/api.js'; -function makeApp(user) { +function makeApp(user, targetUser) { const calls = []; const pool = { async query(sql, params = []) { @@ -13,6 +13,8 @@ function makeApp(user) { if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] }; if (sql.includes('FROM user_roles')) return { rows: [] }; if (sql.startsWith('UPDATE schools SET name')) return { rows: [{ id: params[1], name: params[0] }] }; + if (sql.startsWith('SELECT * FROM classes WHERE id')) return { rows: [{ id: 100, school_id: 2, name: 'Groep 4' }] }; + if (sql.startsWith('SELECT * FROM users WHERE id') && targetUser) return { rows: [targetUser] }; return { rows: [] }; }, }; @@ -50,6 +52,33 @@ test('schoolbeheerder mag geen scholen hernoemen', async () => { await app.close(); }); +test('schoolbeheerder en systeemmanager kunnen aan een klas gekoppeld worden', async () => { + // schoolbeheerder van dezelfde school + const { app, calls } = await makeApp(superUser, { id: 3, role: 'admin', school_id: 2 }); + const res = await app.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, + payload: { userId: 3 } }); + assert.equal(res.statusCode, 200, res.body); + assert.ok(calls.some((c) => c.sql.includes('INSERT INTO class_teachers'))); + await app.close(); + // systeemmanager (geen school) mag ook + const { app: app2 } = await makeApp(superUser, { id: 1, role: 'super', school_id: null }); + const res2 = await app2.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, + payload: { userId: 1 } }); + assert.equal(res2.statusCode, 200, res2.body); + await app2.close(); + // leerlingen niet, en staf van een andere school niet + const { app: app3 } = await makeApp(superUser, { id: 9, role: 'pupil', school_id: 2 }); + const res3 = await app3.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, + payload: { userId: 9 } }); + assert.equal(res3.statusCode, 400); + await app3.close(); + const { app: app4 } = await makeApp(superUser, { id: 4, role: 'admin', school_id: 3 }); + const res4 = await app4.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies, + payload: { userId: 4 } }); + assert.equal(res4.statusCode, 400); + await app4.close(); +}); + test('hernoemen weigert een lege of te lange naam', async () => { const { app } = await makeApp(superUser); const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies,