fix: beperk rollen en klaskoppeling tot staf (v0.3.56-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 38s
- Functie-select in gebruikersbeheer alleen zichtbaar voor staf-rollen (teacher/admin/super) - Klaskoppeling-UI alleen voor staf-rollen - Server: rolwijziging alleen tussen staf-rollen onderling (leerlingen en toekomstige ouder-accounts kunnen nooit promoveren) - Server: klaskoppeling weigert alle niet-staf-rollen expliciet - canPersist() sluit de komende ouderrol uit van borden-opslag - Servertest: ouder-achtige rol krijgt geen promotie en geen klaskoppeling Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0149FgUQvuwxKKEdvQGmNngF
This commit is contained in:
parent
0d644ba41d
commit
a9bf2270d0
5 changed files with 32 additions and 7 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.55-beta
|
||||
0.3.56-beta
|
||||
|
|
|
|||
|
|
@ -206,7 +206,8 @@
|
|||
+ (names.length ? ` · ${names.join(", ")}` : "")
|
||||
+ (u.pending ? ` · ${T("amPending")}` : "")));
|
||||
}
|
||||
if(u.role!=="pupil" && can("users.role.change") && u.id!==currentUser.id){
|
||||
const isStaff = ["teacher","admin","super"].includes(u.role);
|
||||
if(isStaff && can("users.role.change") && u.id!==currentUser.id){
|
||||
/* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super);
|
||||
het aparte extra-functies-systeem is uit de UI gehaald */
|
||||
const rs = h("select","am-sel");
|
||||
|
|
@ -221,7 +222,7 @@
|
|||
/* 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){
|
||||
if(isStaff && 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));
|
||||
|
|
|
|||
|
|
@ -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.55-beta";
|
||||
const VERSION = "0.3.56-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -554,7 +554,9 @@ let DATA_REV = 0;
|
|||
let saveConflict = false;
|
||||
let saveInFlight = false;
|
||||
function canPersist(){
|
||||
return !!currentUser && currentUser.role !== "pupil";
|
||||
/* leerlingen en ouders slaan geen eigen borden-data op; het ouderportaal
|
||||
werkt straks met gekoppelde kind-accounts i.p.v. een eigen blob */
|
||||
return !!currentUser && currentUser.role !== "pupil" && currentUser.role !== "parent";
|
||||
}
|
||||
/* welke secties er sinds de laatste geslaagde save zijn gewijzigd; een save
|
||||
verstuurt alleen die secties i.p.v. altijd de complete blob (die MB's kan
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ export default async function api(app) {
|
|||
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 (u.role === 'pupil') return fail(reply, 400, 'leerlingen horen al bij één klas');
|
||||
if (!['teacher', 'admin', 'super'].includes(u.role)) return fail(reply, 400, 'alleen staf kan aan een klas gekoppeld worden');
|
||||
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]);
|
||||
|
|
@ -526,7 +526,11 @@ export default async function api(app) {
|
|||
await pool.query('DELETE FROM sessions WHERE user_id = $1', [u.id]);
|
||||
return { ok: true, linkCode: code };
|
||||
}
|
||||
if (b.role && can(req.user.allRoles, 'users.role.change') && ['super', 'admin', 'teacher'].includes(b.role) && u.role !== 'pupil')
|
||||
// rolwijziging alleen tussen staf-rollen onderling: leerlingen blijven
|
||||
// leerling en ouder-accounts kunnen nooit naar een beheerrol promoveren
|
||||
if (b.role && can(req.user.allRoles, 'users.role.change')
|
||||
&& ['super', 'admin', 'teacher'].includes(b.role)
|
||||
&& ['super', 'admin', 'teacher'].includes(u.role))
|
||||
await pool.query('UPDATE users SET role = $1, school_id = $2 WHERE id = $3',
|
||||
[b.role, b.role === 'super' ? null : u.school_id, u.id]);
|
||||
// extra functies naast de hoofdrol (bv. een teacher die ook admin-rechten krijgt) -
|
||||
|
|
|
|||
|
|
@ -79,6 +79,24 @@ test('schoolbeheerder en systeemmanager kunnen aan een klas gekoppeld worden', a
|
|||
await app4.close();
|
||||
});
|
||||
|
||||
test('ouder-achtige rol krijgt geen promotie en geen klaskoppeling', async () => {
|
||||
// rolwijziging: alleen staf → staf; een parent-account mag nooit promoveren
|
||||
const { app, calls } = await makeApp(superUser, { id: 7, role: 'parent', school_id: null });
|
||||
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
|
||||
payload: { role: 'admin' } });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.ok(!calls.some((c) => c.sql.startsWith('UPDATE users SET role')),
|
||||
'rolwijziging mag voor een ouder-account niet uitgevoerd worden');
|
||||
await app.close();
|
||||
// klaskoppeling: parent weigeren, net als pupil
|
||||
const { app: app2, calls: calls2 } = await makeApp(superUser, { id: 7, role: 'parent', school_id: null });
|
||||
const res2 = await app2.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies,
|
||||
payload: { userId: 7 } });
|
||||
assert.equal(res2.statusCode, 400);
|
||||
assert.ok(!calls2.some((c) => c.sql.includes('INSERT INTO class_teachers')));
|
||||
await app2.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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue