v0.2.03-beta: kopieerknop voor wachtwoorden + multireset klas/school
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 12s
- Kopieerknop (📋) bij elk getoond wachtwoord/koppelcode, zodat het niet
meer verdwijnt voordat het overgetypt kan worden
- Bugfix: getoond wachtwoord werd bijna direct overschreven doordat de
lijst opnieuw werd opgebouwd vlak nadat het bericht getoond werd (race
condition tussen msg() en reload())
- Nieuw: alle leerlingwachtwoorden van een klas of hele school in één
keer resetten, alleen voor schoolbeheerder en overkoepelend beheerder
- Resultatenpaneel na een multireset met "kopieer alles"-knop (blijft
zichtbaar tot de beheerder het zelf sluit)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
bd7c7e07e7
commit
64acb84f50
4 changed files with 189 additions and 15 deletions
|
|
@ -964,7 +964,10 @@
|
||||||
.am-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;}
|
.am-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;}
|
||||||
.am-head h2{margin:0; font-size:20px;}
|
.am-head h2{margin:0; font-size:20px;}
|
||||||
.am-h3{margin:14px 0 6px; font-size:14px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px;}
|
.am-h3{margin:14px 0 6px; font-size:14px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px;}
|
||||||
.am-msg{min-height:20px; font-size:13.5px; font-weight:800; color:var(--green); margin:4px 0;}
|
.am-msg{
|
||||||
|
min-height:20px; font-size:13.5px; font-weight:800; color:var(--green); margin:4px 0;
|
||||||
|
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
|
||||||
|
}
|
||||||
.am-add{display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:8px 0;}
|
.am-add{display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:8px 0;}
|
||||||
.am-inp,.am-sel{
|
.am-inp,.am-sel{
|
||||||
border:2px solid #cdd9e8; border-radius:10px; padding:8px 10px; font:inherit;
|
border:2px solid #cdd9e8; border-radius:10px; padding:8px 10px; font:inherit;
|
||||||
|
|
@ -1001,4 +1004,21 @@
|
||||||
font-weight:800; font-size:13px;
|
font-weight:800; font-size:13px;
|
||||||
}
|
}
|
||||||
.am-chipx{border:none; background:transparent; color:var(--red); font-weight:900; cursor:pointer; padding:0; font-size:11px;}
|
.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;
|
||||||
|
font:inherit; font-size:13px; cursor:pointer; color:var(--ink); line-height:1;
|
||||||
|
}
|
||||||
|
.am-copy:hover{background:#dbe5f1;}
|
||||||
|
.am-reset{
|
||||||
|
background:#f7fafd; border:2px solid #cdd9e8; border-radius:12px;
|
||||||
|
padding:10px 12px; margin:6px 0; display:flex; flex-direction:column; gap:8px;
|
||||||
|
}
|
||||||
|
.am-reset-head{display:flex; align-items:center; justify-content:space-between; gap:8px;}
|
||||||
|
.am-reset-list{display:flex; flex-direction:column; gap:5px; max-height:260px; overflow:auto;}
|
||||||
|
.am-reset-row{
|
||||||
|
display:flex; gap:8px; align-items:center; background:#fff;
|
||||||
|
border-radius:8px; padding:5px 8px; flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.am-reset-pw{font-weight:800; font-family:monospace; font-size:13px; color:var(--green);}
|
||||||
body.hc .am-row,body.hc .am-chip{border:1.5px solid #000; background:#fff;}
|
body.hc .am-row,body.hc .am-chip{border:1.5px solid #000; background:#fff;}
|
||||||
|
body.hc .am-reset,body.hc .am-reset-row{border:1.5px solid #000; background:#fff;}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@
|
||||||
const modal = document.getElementById("adminModal");
|
const modal = document.getElementById("adminModal");
|
||||||
const btn = document.getElementById("btnAdmin");
|
const btn = document.getElementById("btnAdmin");
|
||||||
let SCHOOLS = [], USERS = [], CLASSES = [], selSchool = null;
|
let SCHOOLS = [], USERS = [], CLASSES = [], selSchool = null;
|
||||||
|
/* resultaat van een laatste multireset (klas/school): blijft zichtbaar
|
||||||
|
totdat de beheerder het venster zelf sluit, zodat wachtwoorden niet
|
||||||
|
meteen weer verdwijnen na een reload() */
|
||||||
|
let resetResults = null;
|
||||||
|
|
||||||
const esc = s => String(s ?? "").replace(/&/g,"&").replace(/</g,"<");
|
const esc = s => String(s ?? "").replace(/&/g,"&").replace(/</g,"<");
|
||||||
const h = (tag, cls, text)=>{
|
const h = (tag, cls, text)=>{
|
||||||
|
|
@ -14,7 +18,36 @@
|
||||||
if(text != null) el.textContent = text;
|
if(text != null) el.textContent = text;
|
||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
const msg = t => { const m = modal.querySelector(".am-msg"); if(m) m.textContent = t; };
|
/* wachtwoorden/koppelcodes zijn maar eenmalig zichtbaar - een kopieerknop
|
||||||
|
zorgt dat ze veilig bewaard kunnen worden voordat het scherm ververst */
|
||||||
|
async function copyText(btn, text){
|
||||||
|
try{ await navigator.clipboard.writeText(text); }
|
||||||
|
catch(e){
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
|
||||||
|
document.body.appendChild(ta); ta.select();
|
||||||
|
try{ document.execCommand("copy"); }catch(_){ /* niets meer te doen */ }
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = "✅";
|
||||||
|
setTimeout(()=>{ btn.textContent = orig; }, 1200);
|
||||||
|
}
|
||||||
|
function copyBtn(text, cls){
|
||||||
|
const b = h("button", cls || "am-copy", "📋");
|
||||||
|
b.type = "button";
|
||||||
|
b.title = T("amCopy");
|
||||||
|
b.addEventListener("click", e=>{ e.preventDefault(); if(text) copyText(b, text); });
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
const msg = (t, copyValue) => {
|
||||||
|
const m = modal.querySelector(".am-msg");
|
||||||
|
if(!m) return;
|
||||||
|
m.innerHTML = "";
|
||||||
|
if(!t) return;
|
||||||
|
m.appendChild(document.createTextNode(t));
|
||||||
|
if(copyValue) m.appendChild(copyBtn(copyValue));
|
||||||
|
};
|
||||||
|
|
||||||
btn.addEventListener("click", async ()=>{
|
btn.addEventListener("click", async ()=>{
|
||||||
if(!currentUser || currentUser.role==="pupil") return;
|
if(!currentUser || currentUser.role==="pupil") return;
|
||||||
|
|
@ -23,7 +56,7 @@
|
||||||
wrap.classList.add("open");
|
wrap.classList.add("open");
|
||||||
});
|
});
|
||||||
wrap.addEventListener("click", e=>{ if(e.target===wrap) wrap.classList.remove("open"); });
|
wrap.addEventListener("click", e=>{ if(e.target===wrap) wrap.classList.remove("open"); });
|
||||||
document.addEventListener("userchange", ()=>{ if(!currentUser) wrap.classList.remove("open"); });
|
document.addEventListener("userchange", ()=>{ if(!currentUser){ wrap.classList.remove("open"); resetResults = null; } });
|
||||||
|
|
||||||
async function reload(){
|
async function reload(){
|
||||||
try{
|
try{
|
||||||
|
|
@ -69,6 +102,11 @@
|
||||||
catch(e){ msg(e.message); }
|
catch(e){ msg(e.message); }
|
||||||
});
|
});
|
||||||
row.appendChild(pw);
|
row.appendChild(pw);
|
||||||
|
const pwCopy = h("button","am-copy","📋");
|
||||||
|
pwCopy.type = "button";
|
||||||
|
pwCopy.title = T("amCopy");
|
||||||
|
pwCopy.addEventListener("click", e=>{ e.preventDefault(); if(pw.value) copyText(pwCopy, pw.value); });
|
||||||
|
row.appendChild(pwCopy);
|
||||||
if(u.everLoggedIn){
|
if(u.everLoggedIn){
|
||||||
const gen = h("button","am-btn","🎲");
|
const gen = h("button","am-btn","🎲");
|
||||||
gen.title = T("amGenerate");
|
gen.title = T("amGenerate");
|
||||||
|
|
@ -76,8 +114,8 @@
|
||||||
try{
|
try{
|
||||||
const { password } = await api("/admin/genpw?role=pupil");
|
const { password } = await api("/admin/genpw?role=pupil");
|
||||||
await api("/admin/users/"+u.id, {method:"PATCH", body:{password}});
|
await api("/admin/users/"+u.id, {method:"PATCH", body:{password}});
|
||||||
msg(`${T("amPwSet")} ${password}`);
|
await reload();
|
||||||
reload();
|
msg(`${T("amPwSet")} ${password}`, password);
|
||||||
}catch(e){ msg(e.message); }
|
}catch(e){ msg(e.message); }
|
||||||
});
|
});
|
||||||
row.appendChild(gen);
|
row.appendChild(gen);
|
||||||
|
|
@ -93,8 +131,8 @@
|
||||||
nc.addEventListener("click", async ()=>{
|
nc.addEventListener("click", async ()=>{
|
||||||
try{
|
try{
|
||||||
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{newLinkCode:true}});
|
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{newLinkCode:true}});
|
||||||
msg(`${T("amCreated")} ${r.linkCode}`);
|
await reload();
|
||||||
reload();
|
msg(`${T("amCreated")} ${r.linkCode}`, r.linkCode);
|
||||||
}catch(e){ msg(e.message); }
|
}catch(e){ msg(e.message); }
|
||||||
});
|
});
|
||||||
row.appendChild(nc);
|
row.appendChild(nc);
|
||||||
|
|
@ -103,8 +141,8 @@
|
||||||
np.addEventListener("click", async ()=>{
|
np.addEventListener("click", async ()=>{
|
||||||
try{
|
try{
|
||||||
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{generatePassword:true}});
|
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{generatePassword:true}});
|
||||||
msg(`${T("amPwSet")} ${r.password}`);
|
await reload();
|
||||||
reload();
|
msg(`${T("amPwSet")} ${r.password}`, r.password);
|
||||||
}catch(e){ msg(e.message); }
|
}catch(e){ msg(e.message); }
|
||||||
});
|
});
|
||||||
row.appendChild(np);
|
row.appendChild(np);
|
||||||
|
|
@ -161,20 +199,54 @@
|
||||||
if(currentUser.role==="super" && role!=="super") body.school = +selSchool;
|
if(currentUser.role==="super" && role!=="super") body.school = +selSchool;
|
||||||
try{
|
try{
|
||||||
const r = await api("/admin/users", { body });
|
const r = await api("/admin/users", { body });
|
||||||
|
name.value = "";
|
||||||
|
if(pwInp) pwInp.value = "";
|
||||||
|
await reload();
|
||||||
msg(role==="pupil"
|
msg(role==="pupil"
|
||||||
? `${T("amPupilMade")} ${r.user.password}`
|
? `${T("amPupilMade")} ${r.user.password}`
|
||||||
: r.user.password
|
: r.user.password
|
||||||
? `${T("amPwSet")} ${r.user.password}`
|
? `${T("amPwSet")} ${r.user.password}`
|
||||||
: `${T("amCreated")} ${r.user.linkCode}`);
|
: `${T("amCreated")} ${r.user.linkCode}`,
|
||||||
name.value = "";
|
r.user.password || r.user.linkCode);
|
||||||
if(pwInp) pwInp.value = "";
|
|
||||||
reload();
|
|
||||||
}catch(e){ msg(e.message); }
|
}catch(e){ msg(e.message); }
|
||||||
});
|
});
|
||||||
f.appendChild(go);
|
f.appendChild(go);
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* paneel met het resultaat van een multireset (klas/school): een lijst
|
||||||
|
nieuwe leerlingwachtwoorden die stuk voor stuk of allemaal ineens
|
||||||
|
gekopieerd kunnen worden. Blijft staan tot de beheerder ✕ klikt. */
|
||||||
|
function resetPanel(){
|
||||||
|
if(!resetResults) return null;
|
||||||
|
const box = h("div","am-reset");
|
||||||
|
const head = h("div","am-reset-head");
|
||||||
|
head.appendChild(h("strong",null, resetResults.title));
|
||||||
|
const close = h("button","wclose","✕");
|
||||||
|
close.addEventListener("click", ()=>{ resetResults = null; render(); });
|
||||||
|
head.appendChild(close);
|
||||||
|
box.appendChild(head);
|
||||||
|
if(!resetResults.entries.length){
|
||||||
|
box.appendChild(h("div","am-msg", T("amResetNone")));
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
const all = resetResults.entries.map(en=>`${en.displayName}\t${en.password}`).join("\n");
|
||||||
|
const copyAll = h("button","am-btn","📋 "+T("amCopyAll"));
|
||||||
|
copyAll.type = "button";
|
||||||
|
copyAll.addEventListener("click", e=>{ e.preventDefault(); copyText(copyAll, all); });
|
||||||
|
box.appendChild(copyAll);
|
||||||
|
const list = h("div","am-reset-list");
|
||||||
|
resetResults.entries.forEach(en=>{
|
||||||
|
const row = h("div","am-reset-row");
|
||||||
|
row.appendChild(h("span","am-name", en.displayName));
|
||||||
|
row.appendChild(h("span","am-reset-pw", en.password));
|
||||||
|
row.appendChild(copyBtn(`${en.displayName}: ${en.password}`));
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
box.appendChild(list);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
function render(){
|
function render(){
|
||||||
modal.innerHTML = "";
|
modal.innerHTML = "";
|
||||||
const head = h("div","am-head");
|
const head = h("div","am-head");
|
||||||
|
|
@ -211,15 +283,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.appendChild(h("div","am-msg"));
|
modal.appendChild(h("div","am-msg"));
|
||||||
|
const rp = resetPanel();
|
||||||
|
if(rp) modal.appendChild(rp);
|
||||||
|
|
||||||
/* klassen (admin/super binnen een school) */
|
/* klassen (admin/super binnen een school) */
|
||||||
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
||||||
|
const canResetPw = ["super","admin"].includes(currentUser.role);
|
||||||
if(schoolChosen){
|
if(schoolChosen){
|
||||||
modal.appendChild(h("h3","am-h3", T("amClasses")));
|
modal.appendChild(h("h3","am-h3", T("amClasses")));
|
||||||
const cwrapEl = h("div","am-classes");
|
const cwrapEl = h("div","am-classes");
|
||||||
CLASSES.forEach(c=>{
|
CLASSES.forEach(c=>{
|
||||||
const chip = h("span","am-chip", c.name);
|
const chip = h("span","am-chip", c.name);
|
||||||
if(["super","admin"].includes(currentUser.role)){
|
if(canResetPw){
|
||||||
|
const rs = h("button","am-chipx","🔄");
|
||||||
|
rs.title = T("amResetClass");
|
||||||
|
rs.addEventListener("click", async ()=>{
|
||||||
|
if(!confirm(`${T("amConfirmReset")} ${c.name}?`)) return;
|
||||||
|
try{
|
||||||
|
const r = await api("/admin/classes/"+c.id+"/reset-passwords", {method:"POST"});
|
||||||
|
await reload();
|
||||||
|
resetResults = { title: `${T("amResetClass")}: ${c.name}`, entries: r.users };
|
||||||
|
render();
|
||||||
|
}catch(e){ msg(e.message); }
|
||||||
|
});
|
||||||
|
chip.appendChild(rs);
|
||||||
const x = h("button","am-chipx","✕");
|
const x = h("button","am-chipx","✕");
|
||||||
x.addEventListener("click", async ()=>{
|
x.addEventListener("click", async ()=>{
|
||||||
try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); }
|
try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); }
|
||||||
|
|
@ -230,7 +317,7 @@
|
||||||
cwrapEl.appendChild(chip);
|
cwrapEl.appendChild(chip);
|
||||||
});
|
});
|
||||||
modal.appendChild(cwrapEl);
|
modal.appendChild(cwrapEl);
|
||||||
if(["super","admin"].includes(currentUser.role)){
|
if(canResetPw){
|
||||||
const f = h("div","am-add");
|
const f = h("div","am-add");
|
||||||
const inp = h("input","am-inp");
|
const inp = h("input","am-inp");
|
||||||
inp.placeholder = T("amNamePh");
|
inp.placeholder = T("amNamePh");
|
||||||
|
|
@ -246,6 +333,22 @@
|
||||||
});
|
});
|
||||||
f.appendChild(go);
|
f.appendChild(go);
|
||||||
modal.appendChild(f);
|
modal.appendChild(f);
|
||||||
|
|
||||||
|
/* multireset: alle leerlingwachtwoorden van de hele school in één keer */
|
||||||
|
const rsAll = h("button","am-btn","🔄 "+T("amResetSchool"));
|
||||||
|
rsAll.type = "button";
|
||||||
|
rsAll.addEventListener("click", async ()=>{
|
||||||
|
const schoolId = currentUser.role==="super" ? selSchool : currentUser.schoolId;
|
||||||
|
if(!schoolId) return;
|
||||||
|
if(!confirm(T("amConfirmResetSchool"))) return;
|
||||||
|
try{
|
||||||
|
const r = await api("/admin/schools/"+schoolId+"/reset-passwords", {method:"POST"});
|
||||||
|
await reload();
|
||||||
|
resetResults = { title: T("amResetSchool"), entries: r.users };
|
||||||
|
render();
|
||||||
|
}catch(e){ msg(e.message); }
|
||||||
|
});
|
||||||
|
modal.appendChild(rsAll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,11 @@ const I18N = {
|
||||||
amNewPw:"nieuw wachtwoord", amPwSet:"Wachtwoord ingesteld:",
|
amNewPw:"nieuw wachtwoord", amPwSet:"Wachtwoord ingesteld:",
|
||||||
amPwOptional:"wachtwoord (leeg = koppelcode)",
|
amPwOptional:"wachtwoord (leeg = koppelcode)",
|
||||||
amConfirmDel:"Weet je zeker dat je deze gebruiker wilt verwijderen?",
|
amConfirmDel:"Weet je zeker dat je deze gebruiker wilt verwijderen?",
|
||||||
|
amCopy:"kopieer", amCopyAll:"kopieer alles",
|
||||||
|
amResetClass:"reset wachtwoorden klas", amResetSchool:"reset alle leerlingwachtwoorden",
|
||||||
|
amConfirmReset:"Alle leerlingwachtwoorden van deze klas vernieuwen?",
|
||||||
|
amConfirmResetSchool:"Alle leerlingwachtwoorden van deze hele school vernieuwen? Dit kan niet ongedaan worden gemaakt.",
|
||||||
|
amResetNone:"Geen leerlingen om te resetten.",
|
||||||
errExists:"Deze naam bestaat al.",
|
errExists:"Deze naam bestaat al.",
|
||||||
errName:"Naam: 2–20 letters of cijfers.",
|
errName:"Naam: 2–20 letters of cijfers.",
|
||||||
errPass:"Wachtwoord: minimaal 4 tekens.",
|
errPass:"Wachtwoord: minimaal 4 tekens.",
|
||||||
|
|
@ -251,6 +256,11 @@ const I18N = {
|
||||||
amNewPw:"new password", amPwSet:"Password set:",
|
amNewPw:"new password", amPwSet:"Password set:",
|
||||||
amPwOptional:"password (empty = link code)",
|
amPwOptional:"password (empty = link code)",
|
||||||
amConfirmDel:"Are you sure you want to delete this user?",
|
amConfirmDel:"Are you sure you want to delete this user?",
|
||||||
|
amCopy:"copy", amCopyAll:"copy all",
|
||||||
|
amResetClass:"reset class passwords", amResetSchool:"reset all pupil passwords",
|
||||||
|
amConfirmReset:"Renew all pupil passwords for this class?",
|
||||||
|
amConfirmResetSchool:"Renew all pupil passwords for this whole school? This cannot be undone.",
|
||||||
|
amResetNone:"No pupils to reset.",
|
||||||
errExists:"This name already exists.",
|
errExists:"This name already exists.",
|
||||||
errName:"Name: 2–20 letters or digits.",
|
errName:"Name: 2–20 letters or digits.",
|
||||||
errPass:"Password: at least 4 characters.",
|
errPass:"Password: at least 4 characters.",
|
||||||
|
|
|
||||||
41
src/api.js
41
src/api.js
|
|
@ -128,6 +128,25 @@ export default async function api(app) {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Multireset: alle leerlingwachtwoorden van een hele school in één keer
|
||||||
|
// vernieuwen. Alleen schoolbeheerder (eigen school) en overkoepelend beheerder.
|
||||||
|
app.post('/admin/schools/:id/reset-passwords', async (req, reply) => {
|
||||||
|
need(req, reply, ['super', 'admin']);
|
||||||
|
const schoolId = req.params.id;
|
||||||
|
if (req.user.role !== '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;
|
||||||
|
const users = [];
|
||||||
|
for (const p of pupils) {
|
||||||
|
const pw = pupilPassword();
|
||||||
|
await pool.query('UPDATE users SET password_plain = $1, password_hash = $2, last_login_at = NULL WHERE id = $3',
|
||||||
|
[pw, await hashPassword(pw), p.id]);
|
||||||
|
users.push({ id: Number(p.id), username: p.username, displayName: p.display_name || p.username, password: pw });
|
||||||
|
}
|
||||||
|
return { users };
|
||||||
|
});
|
||||||
|
|
||||||
// ---- klassen -----------------------------------------------------------------
|
// ---- klassen -----------------------------------------------------------------
|
||||||
app.get('/admin/classes', async (req, reply) => {
|
app.get('/admin/classes', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, ['super', 'admin', 'teacher']);
|
||||||
|
|
@ -166,6 +185,28 @@ export default async function api(app) {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Multireset: alle leerlingwachtwoorden van één klas in één keer vernieuwen.
|
||||||
|
// Alleen schoolbeheerder (eigen school) en overkoepelend beheerder - niet
|
||||||
|
// de groepsleiding, om per ongeluk een hele klas resetten te voorkomen.
|
||||||
|
app.post('/admin/classes/:id/reset-passwords', async (req, reply) => {
|
||||||
|
need(req, reply, ['super', 'admin']);
|
||||||
|
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))
|
||||||
|
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;
|
||||||
|
const users = [];
|
||||||
|
for (const p of pupils) {
|
||||||
|
const pw = pupilPassword();
|
||||||
|
await pool.query('UPDATE users SET password_plain = $1, password_hash = $2, last_login_at = NULL WHERE id = $3',
|
||||||
|
[pw, await hashPassword(pw), p.id]);
|
||||||
|
users.push({ id: Number(p.id), username: p.username, displayName: p.display_name || p.username, password: pw });
|
||||||
|
}
|
||||||
|
return { users };
|
||||||
|
});
|
||||||
|
|
||||||
// Genereer een voorstel-wachtwoord (niet opgeslagen) voor in het aanmaakformulier.
|
// Genereer een voorstel-wachtwoord (niet opgeslagen) voor in het aanmaakformulier.
|
||||||
app.get('/admin/genpw', async (req, reply) => {
|
app.get('/admin/genpw', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, ['super', 'admin', 'teacher']);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue