Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
9 changed files with 146 additions and 29 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.2.20
|
||||
0.2.21
|
||||
|
|
|
|||
9
db/004_user_roles.sql
Normal file
9
db/004_user_roles.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- v0.2.21: extra functies naast de hoofdrol (users.role) - alleen voor stafaccounts.
|
||||
-- Leerlingen krijgen nooit een extra rol; dat wordt in de applicatielaag afgedwongen
|
||||
-- (net als de bestaande regel dat alleen super rollen mag wijzigen).
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, role)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user ON user_roles(user_id);
|
||||
|
|
@ -1042,6 +1042,8 @@
|
|||
border:2px solid #e3eaf3; border-radius:999px; padding:5px 12px;
|
||||
font-weight:800; font-size:13px;
|
||||
}
|
||||
button.am-chip{cursor:pointer; color:var(--muted);}
|
||||
button.am-chip.on{border-color:var(--blue); background:#eaf1fc; color:var(--blue-d);}
|
||||
.am-chipx{border:none; background:transparent; color:var(--red); font-weight:900; cursor:pointer; padding:0; font-size:11px;}
|
||||
.am-copy{
|
||||
border:none; background:#e8eef6; border-radius:8px; padding:5px 8px;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
let resetResults = null;
|
||||
/* actief tabblad, blijft behouden over her-renders heen (na elke actie) */
|
||||
let adminTab = "gebruikers";
|
||||
/* live zoekfilter voor de gebruikerslijst, blijft ook behouden over her-renders */
|
||||
let userFilter = "";
|
||||
|
||||
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
|
||||
const creatableRoles = () =>
|
||||
[...new Set([currentUser.role, ...(currentUser.extraRoles||[])].flatMap(r => CREATABLE_ROLES[r] || []))];
|
||||
|
||||
const esc = s => String(s ?? "").replace(/&/g,"&").replace(/</g,"<");
|
||||
const h = (tag, cls, text)=>{
|
||||
|
|
@ -96,6 +102,23 @@
|
|||
catch(e){ msg(e.message); }
|
||||
});
|
||||
row.appendChild(rs);
|
||||
|
||||
/* extra functies naast de hoofdrol (bv. een teacher die ook beheerdersrechten
|
||||
krijgt) - los van de hoofdrol hierboven, direct toggelbaar per rol */
|
||||
const extraWrap = h("div","am-classes");
|
||||
["teacher","admin","super"].filter(r=>r!==u.role).forEach(r=>{
|
||||
const has = (u.extraRoles||[]).includes(r);
|
||||
const chip = h("button","am-chip", (has?"✓ ":"+ ")+T(roleKey(r)));
|
||||
chip.type = "button";
|
||||
chip.classList.toggle("on", has);
|
||||
chip.addEventListener("click", async ()=>{
|
||||
const next = has ? (u.extraRoles||[]).filter(x=>x!==r) : [...(u.extraRoles||[]), r];
|
||||
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{extraRoles: next}}); reload(); }
|
||||
catch(e){ msg(e.message); }
|
||||
});
|
||||
extraWrap.appendChild(chip);
|
||||
});
|
||||
row.appendChild(extraWrap);
|
||||
}else{
|
||||
row.appendChild(h("span","am-role", T(roleKey(u.role))));
|
||||
}
|
||||
|
|
@ -312,7 +335,7 @@
|
|||
/* overkoepelend beheerders horen niet bij één school, dus het aanmaken ervan
|
||||
staat hier i.p.v. tussen de school-gebonden gebruikers in de Gebruikers-tab
|
||||
(de lijst van bestaande supers blijft wel gewoon in Gebruikers staan) */
|
||||
if(CREATABLE_ROLES[currentUser.role].includes("super")) panel.appendChild(addForm("super"));
|
||||
if(creatableRoles().includes("super")) panel.appendChild(addForm("super"));
|
||||
}
|
||||
|
||||
/* klassen (admin/super binnen een school) */
|
||||
|
|
@ -384,11 +407,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* zoekfilter voor de gebruikerslijst: verbergt niet-matchende rijen en lege groepen
|
||||
via style.display i.p.v. een her-render, zodat het zoekveld focus houdt tijdens
|
||||
het typen (een volledige render() zou het veld elke toets opnieuw aanmaken) */
|
||||
function applyUserFilter(panel){
|
||||
const q = userFilter.trim().toLowerCase();
|
||||
const list = panel.querySelector(".am-list");
|
||||
if(!list) return;
|
||||
let pendingGroup = null, groupVisible = false;
|
||||
[...list.children].forEach(el=>{
|
||||
if(el.classList.contains("am-group")){
|
||||
if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none";
|
||||
pendingGroup = el; groupVisible = false;
|
||||
}else if(el.classList.contains("am-row")){
|
||||
const name = el.querySelector(".am-name");
|
||||
const match = !q || (name && name.textContent.toLowerCase().includes(q));
|
||||
el.style.display = match ? "" : "none";
|
||||
if(match) groupVisible = true;
|
||||
}
|
||||
/* .am-add-wrap (toevoegrij) blijft altijd zichtbaar, ongeacht het filter */
|
||||
});
|
||||
if(pendingGroup) pendingGroup.style.display = groupVisible ? "" : "none";
|
||||
}
|
||||
|
||||
/* gebruikers: lijst (altijd, ook zonder gekozen school) + aanmaak-formulieren.
|
||||
Leerlingen krijgen een aparte behandeling: voor schoolbeheerder/super gegroepeerd
|
||||
per klas (met een inline toevoegrij per klas), voor groepsleiding een platte lijst
|
||||
(die dankzij de server-side scope toch al beperkt is tot de eigen klas(sen)). */
|
||||
function renderGebruikersPanel(panel){
|
||||
const filterBar = h("div","am-add");
|
||||
const filterInp = h("input","am-inp am-filter");
|
||||
filterInp.type = "search";
|
||||
filterInp.placeholder = T("amSearchPh");
|
||||
filterInp.value = userFilter;
|
||||
filterInp.addEventListener("keydown", ev=>ev.stopPropagation());
|
||||
filterInp.addEventListener("input", ()=>{ userFilter = filterInp.value; applyUserFilter(panel); });
|
||||
filterBar.appendChild(filterInp);
|
||||
panel.appendChild(filterBar);
|
||||
|
||||
const list = h("div","am-list");
|
||||
const staffGroups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")]];
|
||||
staffGroups.forEach(([role, label])=>{
|
||||
|
|
@ -400,7 +456,7 @@
|
|||
|
||||
const pupils = USERS.filter(u=>u.role==="pupil");
|
||||
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
||||
const creatable = CREATABLE_ROLES[currentUser.role];
|
||||
const creatable = creatableRoles();
|
||||
const canAddPupil = schoolChosen && creatable.includes("pupil");
|
||||
|
||||
/* per-klas indeling heeft alleen zin met één gekozen school (CLASSES is dan
|
||||
|
|
@ -436,6 +492,7 @@
|
|||
if(schoolChosen){
|
||||
["teacher","admin"].forEach(r=>{ if(creatable.includes(r)) panel.appendChild(addForm(r)); });
|
||||
}
|
||||
applyUserFilter(panel);
|
||||
}
|
||||
|
||||
function switchAdminTab(tab){
|
||||
|
|
|
|||
|
|
@ -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.2.20";
|
||||
const VERSION = "0.2.21";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -137,6 +137,7 @@ const I18N = {
|
|||
amNewSchool:"+ School", amNewClass:"+ Klas", amNewTeacher:"+ Groepsleiding",
|
||||
amNewAdmin:"+ Schoolbeheerder", amNewSuper:"+ Overkoepelend beheerder",
|
||||
amNewPupil:"+ Leerling", amUserPh:"gebruikersnaam…", amNamePh:"naam…",
|
||||
amSearchPh:"🔎 zoek op naam…",
|
||||
amCode:"koppelcode", amNewCode:"nieuwe code", amPw:"wachtwoord",
|
||||
amNoClass:"— geen klas —", amPending:"nog niet geactiveerd",
|
||||
amCouple:"koppel leerkracht", amAllSchools:"— alle scholen —", amSuperGroup:"Overkoepelend",
|
||||
|
|
@ -265,6 +266,7 @@ const I18N = {
|
|||
amNewSchool:"+ School", amNewClass:"+ Class", amNewTeacher:"+ Group leader",
|
||||
amNewAdmin:"+ School admin", amNewSuper:"+ Overarching admin",
|
||||
amNewPupil:"+ Pupil", amUserPh:"username…", amNamePh:"name…",
|
||||
amSearchPh:"🔎 search by name…",
|
||||
amCode:"link code", amNewCode:"new code", amPw:"password",
|
||||
amNoClass:"— no class —", amPending:"not yet activated",
|
||||
amCouple:"link teacher", amAllSchools:"— all schools —", amSuperGroup:"Overarching",
|
||||
|
|
@ -390,7 +392,8 @@ function updateUserUI(){
|
|||
if(!showAdmin && adminTabBtn.classList.contains("active")) switchSettingsTab("account");
|
||||
if(currentUser){
|
||||
document.getElementById("helloName").textContent = currentUser.displayName;
|
||||
document.getElementById("helloRole").textContent = T(roleKey(currentUser.role));
|
||||
const roles = [currentUser.role, ...(currentUser.extraRoles||[])];
|
||||
document.getElementById("helloRole").textContent = roles.map(r=>T(roleKey(r))).join(" + ");
|
||||
document.getElementById("ownPwBlock").style.display = can("password.own.change") ? "flex" : "none";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ const CREATABLE_ROLES = {
|
|||
teacher: ['pupil'],
|
||||
};
|
||||
|
||||
/* can(action) gebruikt de ingelogde gebruiker (currentUser, uit core.js) */
|
||||
/* can(action) gebruikt de ingelogde gebruiker (currentUser, uit core.js) - hoofdrol
|
||||
plus eventuele extra stafrollen (currentUser.extraRoles) */
|
||||
function can(action){
|
||||
return !!currentUser && (PERMISSIONS[action] || []).includes(currentUser.role);
|
||||
if(!currentUser) return false;
|
||||
const roles = [currentUser.role, ...(currentUser.extraRoles||[])];
|
||||
return (PERMISSIONS[action] || []).some(r => roles.includes(r));
|
||||
}
|
||||
|
|
|
|||
70
src/api.js
70
src/api.js
|
|
@ -27,10 +27,20 @@ export default async function api(app) {
|
|||
});
|
||||
const need = (req, reply, roles) => {
|
||||
if (!req.user) { reply.code(401); throw new Error('niet ingelogd'); }
|
||||
if (roles && !roles.includes(req.user.role)) { reply.code(403); throw new Error('geen rechten'); }
|
||||
if (roles && !roles.some((r) => req.user.allRoles.includes(r))) { reply.code(403); throw new Error('geen rechten'); }
|
||||
};
|
||||
// groepsleiding-only regels gelden alleen als iemand *uitsluitend* teacher is;
|
||||
// wie er (via een extra rol) ook admin of super bij heeft, valt onder de
|
||||
// ruimere regels van die rol i.p.v. de leerling-only-beperking van teacher.
|
||||
const teacherOnly = (req) =>
|
||||
req.user.allRoles.includes('teacher')
|
||||
&& !req.user.allRoles.includes('admin')
|
||||
&& !req.user.allRoles.includes('super');
|
||||
// welke rollen iemand mag aanmaken: unie over al hun rollen (hoofd + extra)
|
||||
const creatableRoles = (req) =>
|
||||
[...new Set(req.user.allRoles.flatMap((r) => CREATABLE_ROLES[r] || []))];
|
||||
const sameSchool = (req, u) =>
|
||||
req.user.role === 'super' || Number(u.school_id) === Number(req.user.school_id);
|
||||
req.user.allRoles.includes('super') || Number(u.school_id) === Number(req.user.school_id);
|
||||
|
||||
app.setErrorHandler((err, req, reply) => {
|
||||
if (reply.statusCode >= 400 && reply.statusCode < 500) return reply.send({ error: err.message });
|
||||
|
|
@ -135,7 +145,7 @@ export default async function api(app) {
|
|||
app.post('/admin/schools/:id/reset-passwords', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['schools.resetPasswords']);
|
||||
const schoolId = req.params.id;
|
||||
if (req.user.role !== 'super' && Number(schoolId) !== Number(req.user.school_id))
|
||||
if (!req.user.allRoles.includes('super') && Number(schoolId) !== Number(req.user.school_id))
|
||||
return fail(reply, 403, 'geen rechten');
|
||||
const pupils = (await pool.query(
|
||||
"SELECT * FROM users WHERE school_id = $1 AND role = 'pupil' ORDER BY lower(username)", [schoolId])).rows;
|
||||
|
|
@ -152,7 +162,7 @@ export default async function api(app) {
|
|||
// ---- klassen -----------------------------------------------------------------
|
||||
app.get('/admin/classes', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['classes.view']);
|
||||
const schoolId = req.user.role === 'super' ? req.query.school : req.user.school_id;
|
||||
const schoolId = req.user.allRoles.includes('super') ? req.query.school : req.user.school_id;
|
||||
if (!schoolId) return { classes: [] };
|
||||
const r = await pool.query(
|
||||
`SELECT c.id, c.name,
|
||||
|
|
@ -164,7 +174,7 @@ export default async function api(app) {
|
|||
app.post('/admin/classes', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['classes.manage']);
|
||||
const { name, school } = req.body ?? {};
|
||||
const schoolId = req.user.role === 'super' ? school : req.user.school_id;
|
||||
const schoolId = req.user.allRoles.includes('super') ? school : req.user.school_id;
|
||||
if (!name || !schoolId) return fail(reply, 400, 'naam en school verplicht');
|
||||
const r = await pool.query('INSERT INTO classes (school_id, name) VALUES ($1,$2) RETURNING id, name', [schoolId, name.trim()]);
|
||||
return { class: { id: Number(r.rows[0].id), name: r.rows[0].name } };
|
||||
|
|
@ -173,7 +183,7 @@ export default async function api(app) {
|
|||
need(req, reply, PERMISSIONS['classes.manage']);
|
||||
const r = await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id]);
|
||||
if (!r.rows[0]) return fail(reply, 404, 'klas onbekend');
|
||||
if (req.user.role !== 'super' && Number(r.rows[0].school_id) !== Number(req.user.school_id))
|
||||
if (!req.user.allRoles.includes('super') && Number(r.rows[0].school_id) !== Number(req.user.school_id))
|
||||
return fail(reply, 403, 'geen rechten');
|
||||
await pool.query('DELETE FROM classes WHERE id = $1', [req.params.id]);
|
||||
return { ok: true };
|
||||
|
|
@ -195,7 +205,7 @@ export default async function api(app) {
|
|||
const r = await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id]);
|
||||
const c = r.rows[0];
|
||||
if (!c) return fail(reply, 404, 'klas onbekend');
|
||||
if (req.user.role !== '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');
|
||||
const pupils = (await pool.query(
|
||||
"SELECT * FROM users WHERE class_id = $1 AND role = 'pupil' ORDER BY lower(username)", [c.id])).rows;
|
||||
|
|
@ -222,11 +232,11 @@ export default async function api(app) {
|
|||
app.get('/admin/users', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['users.manage']);
|
||||
let rows;
|
||||
if (req.user.role === 'super') {
|
||||
if (req.user.allRoles.includes('super')) {
|
||||
rows = (req.query.school
|
||||
? await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.query.school])
|
||||
: await pool.query('SELECT * FROM users ORDER BY school_id NULLS FIRST, role, lower(username)')).rows;
|
||||
} else if (req.user.role === 'teacher') {
|
||||
} else if (teacherOnly(req)) {
|
||||
rows = (await pool.query(
|
||||
`SELECT * FROM users
|
||||
WHERE school_id = $1
|
||||
|
|
@ -236,9 +246,18 @@ export default async function api(app) {
|
|||
} else {
|
||||
rows = (await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.user.school_id])).rows;
|
||||
}
|
||||
const canSeePw = can(req.user.role, 'users.manage');
|
||||
const canSeePw = can(req.user.allRoles, 'users.manage');
|
||||
// extra stafrollen per gebruiker erbij ophalen (in één keer, niet per rij)
|
||||
const roleRows = rows.length
|
||||
? (await pool.query('SELECT user_id, role FROM user_roles WHERE user_id = ANY($1)', [rows.map((u) => u.id)])).rows
|
||||
: [];
|
||||
const extraByUser = new Map();
|
||||
roleRows.forEach((r) => {
|
||||
const key = String(r.user_id);
|
||||
extraByUser.set(key, [...(extraByUser.get(key) || []), r.role]);
|
||||
});
|
||||
return { users: rows.map((u) => ({
|
||||
...publicUser(u),
|
||||
...publicUser({ ...u, extra_roles: extraByUser.get(String(u.id)) || [] }),
|
||||
linkCode: u.link_code || null,
|
||||
// leerlingwachtwoord is alleen inzichtelijk zolang het account nog niet
|
||||
// gebruikt is (na de eerste keer inloggen wordt password_plain gewist)
|
||||
|
|
@ -253,8 +272,8 @@ export default async function api(app) {
|
|||
need(req, reply, PERMISSIONS['users.manage']);
|
||||
const b = req.body ?? {};
|
||||
const role = b.role;
|
||||
if (!CREATABLE_ROLES[req.user.role].includes(role)) return fail(reply, 403, 'geen rechten voor deze rol');
|
||||
const schoolId = req.user.role === 'super' ? (role === 'super' ? null : b.school) : req.user.school_id;
|
||||
if (!creatableRoles(req).includes(role)) return fail(reply, 403, 'geen rechten voor deze rol');
|
||||
const schoolId = req.user.allRoles.includes('super') ? (role === 'super' ? null : b.school) : req.user.school_id;
|
||||
if (role !== 'super' && !schoolId) return fail(reply, 400, 'school verplicht');
|
||||
const username = (b.username || '').trim();
|
||||
if (!/^[a-zA-Z0-9_.-]{2,30}$/.test(username)) return fail(reply, 400, 'ongeldige gebruikersnaam');
|
||||
|
|
@ -300,8 +319,8 @@ export default async function api(app) {
|
|||
const u = r.rows[0];
|
||||
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
||||
if (!sameSchool(req, u)) return fail(reply, 403, 'geen rechten');
|
||||
if (req.user.role === 'teacher' && u.role !== 'pupil') return fail(reply, 403, 'groepsleiding beheert alleen leerlingen');
|
||||
if (u.role === 'super' && req.user.role !== 'super') return fail(reply, 403, 'geen rechten');
|
||||
if (teacherOnly(req) && u.role !== 'pupil') return fail(reply, 403, 'groepsleiding beheert alleen leerlingen');
|
||||
if (u.role === 'super' && !req.user.allRoles.includes('super')) return fail(reply, 403, 'geen rechten');
|
||||
const b = req.body ?? {};
|
||||
if (b.displayName) await pool.query('UPDATE users SET display_name = $1 WHERE id = $2', [b.displayName, u.id]);
|
||||
if (b.classId !== undefined && u.role === 'pupil')
|
||||
|
|
@ -314,21 +333,32 @@ export default async function api(app) {
|
|||
}
|
||||
// groepsleiding/beheer: direct een nieuw wachtwoord zetten (zelf gekozen
|
||||
// of gegenereerd) zonder de koppelcode-activatie opnieuw te doorlopen.
|
||||
if ((b.password || b.generatePassword) && u.role !== 'pupil' && can(req.user.role, 'users.staffCredentials')) {
|
||||
if ((b.password || b.generatePassword) && u.role !== 'pupil' && can(req.user.allRoles, 'users.staffCredentials')) {
|
||||
const pw = b.password || staffPassword();
|
||||
const err = checkPassword(pw, u.role);
|
||||
if (err) return fail(reply, 400, err);
|
||||
await pool.query('UPDATE users SET password_hash = $1, link_code = NULL WHERE id = $2', [await hashPassword(pw), u.id]);
|
||||
return { ok: true, password: pw };
|
||||
}
|
||||
if (b.newLinkCode && u.role !== 'pupil' && can(req.user.role, 'users.staffCredentials')) {
|
||||
if (b.newLinkCode && u.role !== 'pupil' && can(req.user.allRoles, 'users.staffCredentials')) {
|
||||
const code = newLinkCode();
|
||||
await pool.query('UPDATE users SET link_code = $1, password_hash = NULL WHERE id = $2', [code, u.id]);
|
||||
return { ok: true, linkCode: code };
|
||||
}
|
||||
if (b.role && can(req.user.role, 'users.role.change') && ['super', 'admin', 'teacher'].includes(b.role) && u.role !== 'pupil')
|
||||
if (b.role && can(req.user.allRoles, 'users.role.change') && ['super', 'admin', 'teacher'].includes(b.role) && u.role !== 'pupil')
|
||||
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) -
|
||||
// alleen voor staf, zelfde recht als hoofdrol-wijziging; hoofdrol zelf telt niet
|
||||
// ook nog als "extra"
|
||||
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);
|
||||
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]);
|
||||
}
|
||||
return { ok: true, extraRoles: extra };
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
|
|
@ -339,8 +369,8 @@ export default async function api(app) {
|
|||
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
||||
if (Number(u.id) === Number(req.user.id)) return fail(reply, 400, 'je kunt jezelf niet verwijderen');
|
||||
if (!sameSchool(req, u)) return fail(reply, 403, 'geen rechten');
|
||||
if (req.user.role === 'teacher' && u.role !== 'pupil') return fail(reply, 403, 'geen rechten');
|
||||
if (u.role === 'super' && req.user.role !== 'super') return fail(reply, 403, 'geen rechten');
|
||||
if (teacherOnly(req) && u.role !== 'pupil') return fail(reply, 403, 'geen rechten');
|
||||
if (u.role === 'super' && !req.user.allRoles.includes('super')) return fail(reply, 403, 'geen rechten');
|
||||
await pool.query('DELETE FROM users WHERE id = $1', [u.id]);
|
||||
return { ok: true };
|
||||
});
|
||||
|
|
|
|||
10
src/auth.js
10
src/auth.js
|
|
@ -88,7 +88,14 @@ export async function userFromRequest(pool, req) {
|
|||
WHERE s.token = $1 AND s.expires_at > now()`,
|
||||
[token],
|
||||
);
|
||||
return r.rows[0] ?? null;
|
||||
const u = r.rows[0];
|
||||
if (!u) return null;
|
||||
// extra stafrollen naast de hoofdrol (role) - alleen relevant voor rechten-checks,
|
||||
// zie src/permissions.js
|
||||
const er = await pool.query('SELECT role FROM user_roles WHERE user_id = $1', [u.id]);
|
||||
u.extra_roles = er.rows.map((row) => row.role);
|
||||
u.allRoles = [u.role, ...u.extra_roles];
|
||||
return u;
|
||||
}
|
||||
|
||||
// Publiek profiel (zonder wachtwoordvelden)
|
||||
|
|
@ -99,6 +106,7 @@ export function publicUser(u) {
|
|||
username: u.username,
|
||||
displayName: u.display_name || u.username,
|
||||
role: u.role,
|
||||
extraRoles: u.extra_roles || [],
|
||||
schoolId: u.school_id == null ? null : Number(u.school_id),
|
||||
classId: u.class_id == null ? null : Number(u.class_id),
|
||||
pending: !u.password_hash && !!u.link_code,
|
||||
|
|
|
|||
|
|
@ -23,4 +23,9 @@ export const CREATABLE_ROLES = {
|
|||
teacher: ['pupil'],
|
||||
};
|
||||
|
||||
export const can = (role, action) => (PERMISSIONS[action] || []).includes(role);
|
||||
// roles mag een losse rol-string zijn (bestaande aanroepen) of een array van alle
|
||||
// rollen die een gebruiker heeft (hoofdrol + eventuele extra stafrollen).
|
||||
export const can = (roles, action) => {
|
||||
const list = Array.isArray(roles) ? roles : [roles];
|
||||
return (PERMISSIONS[action] || []).some(r => list.includes(r));
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue