Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
5 changed files with 72 additions and 8 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.03-beta
|
0.4.04-beta
|
||||||
|
|
|
||||||
|
|
@ -644,10 +644,25 @@
|
||||||
pwWrap.appendChild(gen);
|
pwWrap.appendChild(gen);
|
||||||
f.appendChild(field("amFieldPw", pwWrap));
|
f.appendChild(field("amFieldPw", pwWrap));
|
||||||
}
|
}
|
||||||
|
/* een systeemmanager aanmaken is de meest ingrijpende actie in dit
|
||||||
|
beheer (school-onafhankelijk, alle rechten overal) - vereist daarom
|
||||||
|
als extra stap het eigen wachtwoord van de aanmaker zelf, ter
|
||||||
|
bevestiging. De server controleert dit ook; dit is niet louter cosmetisch. */
|
||||||
|
let confirmInp = null;
|
||||||
|
if(role==="super"){
|
||||||
|
confirmInp = h("input","am-inp");
|
||||||
|
confirmInp.type = "password";
|
||||||
|
confirmInp.autocomplete = "current-password";
|
||||||
|
confirmInp.placeholder = T("amOwnPasswordPh");
|
||||||
|
confirmInp.addEventListener("keydown", ev=>ev.stopPropagation());
|
||||||
|
f.appendChild(field("amOwnPassword", confirmInp));
|
||||||
|
}
|
||||||
const go = h("button","tbtn", label);
|
const go = h("button","tbtn", label);
|
||||||
go.type = "button";
|
go.type = "button";
|
||||||
go.disabled = true;
|
go.disabled = true;
|
||||||
name.addEventListener("input", ()=>{ go.disabled = name.value.trim().length < 2; });
|
const sync = ()=>{ go.disabled = name.value.trim().length < 2 || (confirmInp && !confirmInp.value); };
|
||||||
|
name.addEventListener("input", sync);
|
||||||
|
if(confirmInp) confirmInp.addEventListener("input", sync);
|
||||||
/* fouten horen bij het formulier zelf, niet ergens bovenaan het paneel */
|
/* fouten horen bij het formulier zelf, niet ergens bovenaan het paneel */
|
||||||
const errEl = h("div","am-form-err");
|
const errEl = h("div","am-form-err");
|
||||||
go.addEventListener("click", async ()=>{
|
go.addEventListener("click", async ()=>{
|
||||||
|
|
@ -656,10 +671,12 @@
|
||||||
if(role==="pupil" && cs && cs.value) body.classId = +cs.value;
|
if(role==="pupil" && cs && cs.value) body.classId = +cs.value;
|
||||||
if(pwInp && pwInp.value.trim()) body.password = pwInp.value.trim();
|
if(pwInp && pwInp.value.trim()) body.password = pwInp.value.trim();
|
||||||
if(currentUser.role==="super" && role!=="super") body.school = +selSchool;
|
if(currentUser.role==="super" && role!=="super") body.school = +selSchool;
|
||||||
|
if(confirmInp) body.confirmPassword = confirmInp.value;
|
||||||
try{
|
try{
|
||||||
const r = await api("/admin/users", { body });
|
const r = await api("/admin/users", { body });
|
||||||
name.value = "";
|
name.value = "";
|
||||||
if(pwInp) pwInp.value = "";
|
if(pwInp) pwInp.value = "";
|
||||||
|
if(confirmInp) confirmInp.value = "";
|
||||||
/* het wachtwoord of de koppelcode verschijnt direct bij de nieuwe
|
/* het wachtwoord of de koppelcode verschijnt direct bij de nieuwe
|
||||||
gebruiker in de lijst (rij en groep klappen vanzelf open) */
|
gebruiker in de lijst (rij en groep klappen vanzelf open) */
|
||||||
rowResults.set(r.user.id, {
|
rowResults.set(r.user.id, {
|
||||||
|
|
@ -880,11 +897,6 @@
|
||||||
inp.focus();
|
inp.focus();
|
||||||
});
|
});
|
||||||
panel.appendChild(wrap);
|
panel.appendChild(wrap);
|
||||||
|
|
||||||
/* systeemmanagers 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(creatableRoles().includes("super")) panel.appendChild(addForm("super"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* klassen (admin/super binnen de gekozen school). Een klas hoort altijd bij
|
/* klassen (admin/super binnen de gekozen school). Een klas hoort altijd bij
|
||||||
|
|
@ -1338,6 +1350,10 @@
|
||||||
});
|
});
|
||||||
const supers = USERS.filter(u=>u.role==="super");
|
const supers = USERS.filter(u=>u.role==="super");
|
||||||
if(supers.length) group("gsys", T("amSuperGroup"), supers);
|
if(supers.length) group("gsys", T("amSuperGroup"), supers);
|
||||||
|
/* systeemmanagers horen niet bij één school, dus het aanmaken ervan
|
||||||
|
staat hier (bij "alle scholen") i.p.v. tussen de school-gebonden
|
||||||
|
gebruikers - met een extra bevestigingsstap (eigen wachtwoord). */
|
||||||
|
if(creatableRoles().includes("super")) list.appendChild(addForm("super"));
|
||||||
const parents = USERS.filter(u=>u.role==="parent");
|
const parents = USERS.filter(u=>u.role==="parent");
|
||||||
if(parents.length) group("gparents", T("amParents"), parents);
|
if(parents.length) group("gparents", T("amParents"), parents);
|
||||||
const loose = USERS.filter(u=>["teacher","admin","pupil"].includes(u.role) && u.schoolId==null);
|
const loose = USERS.filter(u=>["teacher","admin","pupil"].includes(u.role) && u.schoolId==null);
|
||||||
|
|
|
||||||
|
|
@ -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.4.03-beta";
|
const VERSION = "0.4.04-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -308,6 +308,7 @@ const I18N = {
|
||||||
amPwHidden:"al gebruikt", amGenerate:"genereer wachtwoord",
|
amPwHidden:"al gebruikt", amGenerate:"genereer wachtwoord",
|
||||||
amNewPw:"nieuw wachtwoord", amPwSet:"Wachtwoord ingesteld:",
|
amNewPw:"nieuw wachtwoord", amPwSet:"Wachtwoord ingesteld:",
|
||||||
amPwOptional:"wachtwoord (leeg = koppelcode)",
|
amPwOptional:"wachtwoord (leeg = koppelcode)",
|
||||||
|
amOwnPassword:"Eigen wachtwoord ter bevestiging", amOwnPasswordPh:"jouw huidige wachtwoord…",
|
||||||
amAssignments:"Toewijzingen", amAssignClasses:"Per klas (standaard)", amAssignPupils:"Per leerling (uitzondering)",
|
amAssignments:"Toewijzingen", amAssignClasses:"Per klas (standaard)", amAssignPupils:"Per leerling (uitzondering)",
|
||||||
amPickPupil:"— kies een leerling —", amNoAssignment:"— geen toewijzing —",
|
amPickPupil:"— kies een leerling —", amNoAssignment:"— geen toewijzing —",
|
||||||
amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.",
|
amAssignedAs:"Toegewezen:", amAssign:"Toewijzen", amAssignSaved:"Toewijzing opgeslagen.",
|
||||||
|
|
@ -606,6 +607,7 @@ const I18N = {
|
||||||
amPwHidden:"already used", amGenerate:"generate password",
|
amPwHidden:"already used", amGenerate:"generate password",
|
||||||
amNewPw:"new password", amPwSet:"Password set:",
|
amNewPw:"new password", amPwSet:"Password set:",
|
||||||
amPwOptional:"password (empty = link code)",
|
amPwOptional:"password (empty = link code)",
|
||||||
|
amOwnPassword:"Your own password, to confirm", amOwnPasswordPh:"your current password…",
|
||||||
amAssignments:"Assignments", amAssignClasses:"Per class (default)", amAssignPupils:"Per pupil (override)",
|
amAssignments:"Assignments", amAssignClasses:"Per class (default)", amAssignPupils:"Per pupil (override)",
|
||||||
amPickPupil:"— pick a pupil —", amNoAssignment:"— no assignment —",
|
amPickPupil:"— pick a pupil —", amNoAssignment:"— no assignment —",
|
||||||
amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.",
|
amAssignedAs:"Assigned:", amAssign:"Assign", amAssignSaved:"Assignment saved.",
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,14 @@ export default async function api(app) {
|
||||||
const b = req.body ?? {};
|
const b = req.body ?? {};
|
||||||
const role = b.role;
|
const role = b.role;
|
||||||
if (!creatableRoles(req).includes(role)) return fail(reply, 403, 'geen rechten voor deze rol');
|
if (!creatableRoles(req).includes(role)) return fail(reply, 403, 'geen rechten voor deze rol');
|
||||||
|
// Extra veiligheidsstap voor de meest ingrijpende actie in dit beheer: een
|
||||||
|
// nieuwe systeemmanager aanmaken vereist het eigen wachtwoord van de
|
||||||
|
// aanmaker, ter bevestiging - los van (en aanvullend op) creatableRoles(),
|
||||||
|
// die al bepaalt dát alleen een systeemmanager dit mag proberen.
|
||||||
|
if (role === 'super') {
|
||||||
|
const ok = await verifyPassword(b.confirmPassword, req.user.password_hash);
|
||||||
|
if (!ok) return fail(reply, 403, 'eigen wachtwoord onjuist');
|
||||||
|
}
|
||||||
const schoolId = req.user.allRoles.includes('super') ? (role === 'super' ? null : b.school) : req.user.school_id;
|
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');
|
if (role !== 'super' && !schoolId) return fail(reply, 400, 'school verplicht');
|
||||||
const username = (b.username || '').trim();
|
const username = (b.username || '').trim();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Fastify from 'fastify';
|
||||||
import cookie from '@fastify/cookie';
|
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';
|
||||||
|
import { hashPassword } from '../src/auth.js';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
function makeApp(user, targetUser) {
|
function makeApp(user, targetUser) {
|
||||||
|
|
@ -320,3 +321,40 @@ test('hernoemen weigert een lege of te lange naam', async () => {
|
||||||
assert.equal(res2.statusCode, 400);
|
assert.equal(res2.statusCode, 400);
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('systeemmanager aanmaken vereist het eigen wachtwoord van de aanmaker ter bevestiging', async () => {
|
||||||
|
const ownHash = await hashPassword('correcthorse1');
|
||||||
|
const superWithPw = { ...superUser, password_hash: ownHash };
|
||||||
|
// fout eigen wachtwoord: geweigerd, geen insert
|
||||||
|
const { app, calls } = await makeApp(superWithPw);
|
||||||
|
const res = await app.inject({ method: 'POST', url: '/api/admin/users', cookies,
|
||||||
|
payload: { role: 'super', username: 'nieuwesm', confirmPassword: 'helemaal-fout' } });
|
||||||
|
assert.equal(res.statusCode, 403, res.body);
|
||||||
|
assert.match(res.json().error, /wachtwoord onjuist/);
|
||||||
|
assert.ok(!calls.some((c) => c.sql.startsWith('INSERT INTO users')));
|
||||||
|
await app.close();
|
||||||
|
// geen eigen wachtwoord meegestuurd: ook geweigerd
|
||||||
|
const { app: app2, calls: calls2 } = await makeApp(superWithPw);
|
||||||
|
const res2 = await app2.inject({ method: 'POST', url: '/api/admin/users', cookies,
|
||||||
|
payload: { role: 'super', username: 'nieuwesm' } });
|
||||||
|
assert.equal(res2.statusCode, 403, res2.body);
|
||||||
|
assert.ok(!calls2.some((c) => c.sql.startsWith('INSERT INTO users')));
|
||||||
|
await app2.close();
|
||||||
|
// correct eigen wachtwoord: mag door naar de insert
|
||||||
|
const { app: app3, calls: calls3 } = await makeApp(superWithPw);
|
||||||
|
const res3 = await app3.inject({ method: 'POST', url: '/api/admin/users', cookies,
|
||||||
|
payload: { role: 'super', username: 'nieuwesm', confirmPassword: 'correcthorse1' } });
|
||||||
|
assert.equal(res3.statusCode, 200, res3.body);
|
||||||
|
assert.ok(calls3.some((c) => c.sql.startsWith('INSERT INTO users')));
|
||||||
|
await app3.close();
|
||||||
|
// een schoolbeheerder mag sowieso geen systeemmanager aanmaken (creatableRoles),
|
||||||
|
// ongeacht wat er als confirmPassword meegestuurd wordt
|
||||||
|
const adminHash = await hashPassword('adminwachtwoord1');
|
||||||
|
const { app: app4, calls: calls4 } = await makeApp({ ...admin, password_hash: adminHash });
|
||||||
|
const res4 = await app4.inject({ method: 'POST', url: '/api/admin/users', cookies,
|
||||||
|
payload: { role: 'super', username: 'nieuwesm', confirmPassword: 'adminwachtwoord1' } });
|
||||||
|
assert.equal(res4.statusCode, 403, res4.body);
|
||||||
|
assert.match(res4.json().error, /geen rechten voor deze rol/);
|
||||||
|
assert.ok(!calls4.some((c) => c.sql.startsWith('INSERT INTO users')));
|
||||||
|
await app4.close();
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue