Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5
7 changed files with 114 additions and 13 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.06-beta
|
||||
0.4.07-beta
|
||||
|
|
|
|||
44
db/017_class_teacher_roles.sql
Normal file
44
db/017_class_teacher_roles.sql
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
-- v0.4.07-beta: schoolbeheerder (en systeemmanager) konden niet aan een klas
|
||||
-- gekoppeld worden, met een verwarrende "groepsleiding en klas moeten bij
|
||||
-- dezelfde school horen" - ook al staat dit in de applicatielaag (POST
|
||||
-- /admin/classes/:id/teachers, src/api.js) al langer voor elke stafrol open.
|
||||
--
|
||||
-- Oorzaak: de trigger teach_validate_class_teacher() (uit 006) is destijds
|
||||
-- geschreven toen alleen groepsleiding aan een klas gekoppeld kon worden, en
|
||||
-- is nooit meegegroeid toen schoolbeheerder/systeemmanager daar later bij
|
||||
-- kwamen - de trigger stond dus in de weg van iets wat de app allang toestond.
|
||||
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM classes c
|
||||
JOIN users u ON u.id = NEW.user_id
|
||||
WHERE c.id = NEW.class_id
|
||||
AND (
|
||||
-- systeemmanager is school-onafhankelijk, mag overal aan gekoppeld worden
|
||||
u.role = 'super'
|
||||
OR (
|
||||
u.school_id = c.school_id
|
||||
AND (
|
||||
u.role IN ('teacher', 'admin')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM user_roles ur
|
||||
WHERE ur.user_id = u.id AND ur.role IN ('teacher', 'admin')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'alleen staf uit dezelfde school (of een systeemmanager) kan aan een klas gekoppeld worden';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- trigger zelf blijft ongewijzigd (verwijst naar de herdefinieerde functie),
|
||||
-- alleen opnieuw aangemaakt zodat deze migratie op zichzelf leesbaar is
|
||||
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
|
||||
CREATE TRIGGER trg_class_teachers_tenant
|
||||
BEFORE INSERT OR UPDATE ON class_teachers
|
||||
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();
|
||||
|
|
@ -1276,6 +1276,13 @@
|
|||
}
|
||||
.am-schoolbar-lbl{font-size:16px;}
|
||||
.am-schoolbar .am-sel{flex:1; min-width:0;}
|
||||
/* de dichtgeklapte "+ ..."-knop hoort rechts, niet als volle-breedte-rij
|
||||
links uitgelijnd zoals een gebruikersrij - dat maakt meteen duidelijk dat
|
||||
het om een aparte actie gaat, niet om nóg een item in de lijst. Eenmaal
|
||||
uitgeklapt neemt het formulier zelf wél de volle breedte (align-self niet
|
||||
gezet op .am-add, dus terug naar de stretch van de kolom-flex hierboven). */
|
||||
.am-add-wrap{display:flex; flex-direction:column;}
|
||||
.am-add-wrap > .tbtn{align-self:flex-end;}
|
||||
/* een uitgeklapt toevoegformulier moet nooit op een bestaande-gebruikersrij
|
||||
lijken: duidelijk eigen kader (accentkleur, stippellijn) i.p.v. de neutrale
|
||||
achtergrond van .am-row/.am-row-tools, plus een titel die herhaalt wát je
|
||||
|
|
@ -1905,7 +1912,10 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .wo
|
|||
#settingsModal .am-list{gap:0; border:1px solid var(--line); border-radius:var(--radius-s); overflow:hidden; background:var(--surface);}
|
||||
#settingsModal .am-row{
|
||||
background:transparent; border-radius:0; box-shadow:none;
|
||||
border-bottom:1px solid var(--line); margin:0; padding:7px 10px; font-size:13.5px;
|
||||
/* meer verticale ruimte dan voorheen (was 7px): rijen zonder gap ertussen
|
||||
stonden zo dicht op elkaar dat je makkelijk de verkeerde raakte, zeker
|
||||
op een telefoon */
|
||||
border-bottom:1px solid var(--line); margin:0; padding:12px 10px; font-size:13.5px;
|
||||
}
|
||||
#settingsModal .am-list > .am-row:last-child,
|
||||
#settingsModal .am-list > .am-add-wrap:last-child{border-bottom:none;}
|
||||
|
|
|
|||
|
|
@ -384,11 +384,15 @@
|
|||
});
|
||||
tools.appendChild(field("amFieldSchool", ss));
|
||||
}
|
||||
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 */
|
||||
/* rolwijziging: alleen tussen groepsleiding/schoolbeheerder onderling.
|
||||
Systeemmanager is hier bewust nooit een optie - die rol ontstaat alleen
|
||||
via de aparte "Nieuwe systeemmanager"-aanmaak (met bevestiging van het
|
||||
eigen wachtwoord), nooit door een bestaand account te promoveren. Een
|
||||
bestaande systeemmanager toont dit veld daarom ook niet: er is geen
|
||||
veilige weg om die rol via dit simpele dropdown-veld te wijzigen. */
|
||||
if(isStaff && u.role!=="super" && can("users.role.change") && u.id!==currentUser.id){
|
||||
const rs = h("select","am-sel");
|
||||
["teacher","admin","super"].forEach(r=>rs.appendChild(new Option(T(roleKey(r)), r)));
|
||||
["teacher","admin"].forEach(r=>rs.appendChild(new Option(T(roleKey(r)), r)));
|
||||
rs.value = u.role;
|
||||
rs.addEventListener("change", async ()=>{
|
||||
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{role: rs.value}}); reload(); }
|
||||
|
|
|
|||
|
|
@ -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.4.06-beta";
|
||||
const VERSION = "0.4.07-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
|
|||
17
src/api.js
17
src/api.js
|
|
@ -661,17 +661,22 @@ export default async function api(app) {
|
|||
}
|
||||
}
|
||||
// rolwijziging alleen tussen staf-rollen onderling: leerlingen blijven
|
||||
// leerling en ouder-accounts kunnen nooit naar een beheerrol promoveren
|
||||
// leerling en ouder-accounts kunnen nooit naar een beheerrol promoveren.
|
||||
// 'super' is hier bewust GEEN geldig doel: een systeemmanager aanmaken kan
|
||||
// alleen via POST /admin/users, met het eigen-wachtwoord-bevestigingsstap -
|
||||
// niet door een bestaande schoolbeheerder/groepsleiding via deze
|
||||
// eenvoudige rolwijziging te promoveren. Demoteren van een bestaande
|
||||
// systeemmanager naar admin/teacher blijft wel mogelijk.
|
||||
if (b.role && can(req.user.allRoles, 'users.role.change')
|
||||
&& ['super', 'admin', 'teacher'].includes(b.role)
|
||||
&& ['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]);
|
||||
await pool.query('UPDATE users SET role = $1 WHERE id = $2', [b.role, u.id]);
|
||||
// extra functies naast de hoofdrol (bv. een teacher die ook admin-rechten krijgt) -
|
||||
// alleen voor staf, zelfde recht als hoofdrol-wijziging; hoofdrol zelf telt niet
|
||||
// ook nog als "extra"
|
||||
// ook nog als "extra". Ook hier is 'super' bewust geen toegestane extra rol,
|
||||
// om dezelfde reden als hierboven.
|
||||
if (b.extraRoles !== undefined && can(req.user.allRoles, 'users.role.change') && u.role !== 'pupil') {
|
||||
const extra = [...new Set(b.extraRoles)].filter((r) => ['super', 'admin', 'teacher'].includes(r) && r !== u.role);
|
||||
const extra = [...new Set(b.extraRoles)].filter((r) => ['admin', 'teacher'].includes(r) && r !== u.role);
|
||||
await pool.query('DELETE FROM user_roles WHERE user_id = $1', [u.id]);
|
||||
for (const role of extra) {
|
||||
await pool.query('INSERT INTO user_roles (user_id, role) VALUES ($1, $2) ON CONFLICT DO NOTHING', [u.id, role]);
|
||||
|
|
|
|||
|
|
@ -116,6 +116,33 @@ test('ouder-achtige rol krijgt geen promotie en geen klaskoppeling', async () =>
|
|||
await app2.close();
|
||||
});
|
||||
|
||||
test('een bestaande groepsleiding of schoolbeheerder kan via rolwijziging nooit systeemmanager worden', async () => {
|
||||
// zelfs een systeemmanager mag dit niet via de gewone rolwijziging - alleen
|
||||
// via POST /admin/users (met de eigen-wachtwoord-bevestiging) mag een
|
||||
// nieuwe systeemmanager ontstaan
|
||||
const { app, calls } = await makeApp(superUser, { id: 7, role: 'teacher', school_id: 2 });
|
||||
const res = await app.inject({ method: 'PATCH', url: '/api/admin/users/7', cookies,
|
||||
payload: { role: 'super' } });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.ok(!calls.some((c) => c.sql.startsWith('UPDATE users SET role')),
|
||||
'promotie naar systeemmanager via rolwijziging moet genegeerd worden');
|
||||
await app.close();
|
||||
const { app: app2, calls: calls2 } = await makeApp(superUser, { id: 8, role: 'admin', school_id: 2 });
|
||||
const res2 = await app2.inject({ method: 'PATCH', url: '/api/admin/users/8', cookies,
|
||||
payload: { role: 'super' } });
|
||||
assert.equal(res2.statusCode, 200, res2.body);
|
||||
assert.ok(!calls2.some((c) => c.sql.startsWith('UPDATE users SET role')));
|
||||
await app2.close();
|
||||
// ook niet via het extra-rollen-mechanisme
|
||||
const { app: app3, calls: calls3 } = await makeApp(superUser, { id: 8, role: 'admin', school_id: 2 });
|
||||
const res3 = await app3.inject({ method: 'PATCH', url: '/api/admin/users/8', cookies,
|
||||
payload: { extraRoles: ['super', 'teacher'] } });
|
||||
assert.equal(res3.statusCode, 200, res3.body);
|
||||
assert.deepEqual(res3.json().extraRoles, ['teacher']);
|
||||
assert.ok(!calls3.some((c) => c.sql.startsWith('INSERT INTO user_roles') && c.params.includes('super')));
|
||||
await app3.close();
|
||||
});
|
||||
|
||||
test('systeemmanager koppelt een gebruiker aan een school en maakt hem weer los', async () => {
|
||||
// koppelen: school bestaat -> update + klas-/toewijzing-opruiming
|
||||
const { app, calls } = await makeApp(superUser, { id: 7, role: 'teacher', school_id: null, username: 'juf' });
|
||||
|
|
@ -296,6 +323,17 @@ test('migratie 016 hernoemt bestaande dubbele klasnamen vóórdat de unieke inde
|
|||
assert.doesNotMatch(sql, /DELETE FROM classes/);
|
||||
});
|
||||
|
||||
test('migratie 017 laat schoolbeheerder en systeemmanager ook echt via de database toe aan een klas', async () => {
|
||||
// regressie: de trigger uit 006 stond alleen 'teacher' toe en blokkeerde
|
||||
// daarmee (met een verwarrende foutmelding) wat de applicatielaag
|
||||
// (POST /admin/classes/:id/teachers) allang toestond voor admin/super -
|
||||
// een mismatch die de gemockte-pool-tests principieel nooit konden vangen.
|
||||
const sql = await readFile('db/017_class_teacher_roles.sql', 'utf8');
|
||||
assert.match(sql, /u\.role = 'super'/);
|
||||
assert.match(sql, /u\.role IN \('teacher', 'admin'\)/);
|
||||
assert.match(sql, /CREATE OR REPLACE FUNCTION teach_validate_class_teacher/);
|
||||
});
|
||||
|
||||
test('klas aanmaken weigert een dubbele naam binnen dezelfde school (409)', async () => {
|
||||
const { app } = await makeAppWithSchool(superUser, null);
|
||||
const pool = app.pg;
|
||||
|
|
|
|||
Loading…
Reference in a new issue