v0.3.38-beta: koppel ook schoolbeheerders en systeemmanagers aan klassen
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 23s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 23s
- 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
This commit is contained in:
parent
804ffb98f6
commit
7287e49e0c
5 changed files with 45 additions and 14 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.37-beta
|
0.3.38-beta
|
||||||
|
|
|
||||||
|
|
@ -198,15 +198,13 @@
|
||||||
if(u.role==="pupil"){
|
if(u.role==="pupil"){
|
||||||
const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId));
|
const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId));
|
||||||
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : "")));
|
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : "")));
|
||||||
}else if(u.role==="teacher"){
|
}else{
|
||||||
const names = (u.classIds||[])
|
const names = (u.classIds||[])
|
||||||
.map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name)
|
.map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
row.appendChild(h("span","am-role", T(roleKey(u.role))
|
row.appendChild(h("span","am-role", T(roleKey(u.role))
|
||||||
+ (names.length ? ` · ${names.join(", ")}` : "")
|
+ (names.length ? ` · ${names.join(", ")}` : "")
|
||||||
+ (u.pending ? ` · ${T("amPending")}` : "")));
|
+ (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){
|
if(u.role!=="pupil" && can("users.role.change") && u.id!==currentUser.id){
|
||||||
/* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super);
|
/* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super);
|
||||||
|
|
@ -220,9 +218,10 @@
|
||||||
});
|
});
|
||||||
tools.appendChild(field("amFieldRole", rs));
|
tools.appendChild(field("amFieldRole", rs));
|
||||||
}
|
}
|
||||||
/* groepsleiding eenvoudig aan klassen koppelen: huidige klassen als
|
/* elke staf-rol eenvoudig aan klassen koppelen (groepsleiding,
|
||||||
chips met ✕, en een select om een klas toe te voegen */
|
schoolbeheerder én systeemmanager): huidige klassen als chips met ✕,
|
||||||
if(u.role==="teacher" && can("classes.manage") && CLASSES.length){
|
en een select om een klas toe te voegen */
|
||||||
|
if(u.role!=="pupil" && can("classes.manage") && CLASSES.length){
|
||||||
const wrap = h("div","am-classes");
|
const wrap = h("div","am-classes");
|
||||||
(u.classIds||[]).forEach(cid=>{
|
(u.classIds||[]).forEach(cid=>{
|
||||||
const c = CLASSES.find(x=>Number(x.id)===Number(cid));
|
const c = CLASSES.find(x=>Number(x.id)===Number(cid));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
/* 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. */
|
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(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
|
||||||
13
src/api.js
13
src/api.js
|
|
@ -331,19 +331,22 @@ export default async function api(app) {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
// groepsleiding aan klas koppelen/loskoppelen
|
// 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) => {
|
app.post('/admin/classes/:id/teachers', async (req, reply) => {
|
||||||
need(req, reply, PERMISSIONS['classes.manage']);
|
need(req, reply, PERMISSIONS['classes.manage']);
|
||||||
const { userId, remove } = req.body ?? {};
|
const { userId, remove } = req.body ?? {};
|
||||||
if (!userId) return fail(reply, 400, 'gebruiker verplicht');
|
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 c = (await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id])).rows[0];
|
||||||
const u = (await pool.query(`SELECT u.*, EXISTS (
|
const u = (await pool.query('SELECT * FROM users WHERE id = $1', [userId])).rows[0];
|
||||||
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];
|
|
||||||
if (!c || !u) return fail(reply, 404, 'klas of gebruiker onbekend');
|
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))
|
if (!req.user.allRoles.includes('super') && Number(c.school_id) !== Number(req.user.school_id))
|
||||||
return fail(reply, 403, 'geen rechten');
|
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 === 'pupil') return fail(reply, 400, 'leerlingen horen al bij één klas');
|
||||||
if (u.role !== 'teacher' && !u.extra_teacher) return fail(reply, 400, 'gebruiker is geen groepsleiding');
|
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]);
|
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]);
|
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 };
|
return { ok: true };
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import cookie from '@fastify/cookie';
|
||||||
import rateLimit from '@fastify/rate-limit';
|
import rateLimit from '@fastify/rate-limit';
|
||||||
import api from '../src/api.js';
|
import api from '../src/api.js';
|
||||||
|
|
||||||
function makeApp(user) {
|
function makeApp(user, targetUser) {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const pool = {
|
const pool = {
|
||||||
async query(sql, params = []) {
|
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 sessions s JOIN users u')) return { rows: [user] };
|
||||||
if (sql.includes('FROM user_roles')) return { rows: [] };
|
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('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: [] };
|
return { rows: [] };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -50,6 +52,33 @@ test('schoolbeheerder mag geen scholen hernoemen', async () => {
|
||||||
await app.close();
|
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 () => {
|
test('hernoemen weigert een lege of te lange naam', async () => {
|
||||||
const { app } = await makeApp(superUser);
|
const { app } = await makeApp(superUser);
|
||||||
const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies,
|
const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue