All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
- Beheerders kunnen bij groepsleiding/schoolbeheer/super meteen zelf een wachtwoord instellen of genereren, als alternatief voor de koppelcode. - Nieuwe directe wachtwoord-reset voor deze rollen, los van koppelcode-reset. - Leerling-wachtwoord is na de eerste keer inloggen niet langer inzichtelijk voor groepsleiding/beheer; alleen opnieuw genereren kan dan nog. - Wachtwoordeisen aangescherpt en overal consistent gecontroleerd (leerling min. 4 tekens, groepsleiding/beheer min. 8 met letter en cijfer). - Bevestigingsveld en toon/verberg-knop bij accountactivatie en eigen wachtwoord wijzigen, om typefouten te voorkomen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
/* teach - beheerpaneel (👥): gebruikers, klassen en scholen.
|
|
Zichtbaar voor super (alles), admin (eigen school) en teacher (leerlingen). */
|
|
"use strict";
|
|
(function(){
|
|
const wrap = document.getElementById("adminWrap");
|
|
const modal = document.getElementById("adminModal");
|
|
const btn = document.getElementById("btnAdmin");
|
|
let SCHOOLS = [], USERS = [], CLASSES = [], selSchool = null;
|
|
|
|
const esc = s => String(s ?? "").replace(/&/g,"&").replace(/</g,"<");
|
|
const h = (tag, cls, text)=>{
|
|
const el = document.createElement(tag);
|
|
if(cls) el.className = cls;
|
|
if(text != null) el.textContent = text;
|
|
return el;
|
|
};
|
|
const msg = t => { const m = modal.querySelector(".am-msg"); if(m) m.textContent = t; };
|
|
|
|
btn.addEventListener("click", async ()=>{
|
|
if(!currentUser || currentUser.role==="pupil") return;
|
|
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
|
await reload();
|
|
wrap.classList.add("open");
|
|
});
|
|
wrap.addEventListener("click", e=>{ if(e.target===wrap) wrap.classList.remove("open"); });
|
|
document.addEventListener("userchange", ()=>{ if(!currentUser) wrap.classList.remove("open"); });
|
|
|
|
async function reload(){
|
|
try{
|
|
if(currentUser.role==="super") SCHOOLS = await api("/schools");
|
|
const q = currentUser.role==="super" && selSchool ? "?school="+selSchool : "";
|
|
USERS = (await api("/admin/users"+q)).users;
|
|
const cq = currentUser.role==="super" ? (selSchool ? "?school="+selSchool : "?school=0") : "";
|
|
CLASSES = (await api("/admin/classes"+cq)).classes || [];
|
|
}catch(e){ USERS = []; CLASSES = []; }
|
|
render();
|
|
}
|
|
|
|
function classSel(current){
|
|
const sel = h("select","am-sel");
|
|
sel.appendChild(new Option(T("amNoClass"), ""));
|
|
CLASSES.forEach(c=>sel.appendChild(new Option(c.name, c.id)));
|
|
sel.value = current ?? "";
|
|
return sel;
|
|
}
|
|
|
|
function userRow(u){
|
|
const row = h("div","am-row");
|
|
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
|
|
row.appendChild(h("span","am-role", T(roleKey(u.role))));
|
|
if(u.role==="pupil"){
|
|
/* klas (uitwisselen met andere klassen) */
|
|
const cs = classSel(u.classId);
|
|
cs.addEventListener("change", async ()=>{
|
|
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{classId: cs.value ? +cs.value : null}}); }
|
|
catch(e){ msg(e.message); }
|
|
});
|
|
row.appendChild(cs);
|
|
/* wachtwoord inzien en aanpassen - alleen zichtbaar zolang de leerling
|
|
nog niet is ingelogd; daarna kan alleen een nieuw wachtwoord gezet worden */
|
|
const pw = h("input","am-pw");
|
|
pw.value = u.password || "";
|
|
pw.placeholder = u.everLoggedIn ? T("amPwHidden") : "";
|
|
pw.title = T("amPw");
|
|
pw.addEventListener("keydown", ev=>ev.stopPropagation());
|
|
pw.addEventListener("change", async ()=>{
|
|
if(pw.value.length < 4) return;
|
|
try{ await api("/admin/users/"+u.id, {method:"PATCH", body:{password: pw.value}}); reload(); }
|
|
catch(e){ msg(e.message); }
|
|
});
|
|
row.appendChild(pw);
|
|
if(u.everLoggedIn){
|
|
const gen = h("button","am-btn","🎲");
|
|
gen.title = T("amGenerate");
|
|
gen.addEventListener("click", async ()=>{
|
|
try{
|
|
const { password } = await api("/admin/genpw?role=pupil");
|
|
await api("/admin/users/"+u.id, {method:"PATCH", body:{password}});
|
|
msg(`${T("amPwSet")} ${password}`);
|
|
reload();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
row.appendChild(gen);
|
|
}
|
|
}else{
|
|
if(u.pending && u.linkCode){
|
|
row.appendChild(h("span","am-code", T("amCode")+": "+u.linkCode));
|
|
}else if(u.pending){
|
|
row.appendChild(h("span","am-code", T("amPending")));
|
|
}
|
|
if(["super","admin"].includes(currentUser.role) && u.role!=="pupil" && u.id!==currentUser.id){
|
|
const nc = h("button","am-btn", T("amNewCode"));
|
|
nc.addEventListener("click", async ()=>{
|
|
try{
|
|
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{newLinkCode:true}});
|
|
msg(`${T("amCreated")} ${r.linkCode}`);
|
|
reload();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
row.appendChild(nc);
|
|
/* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */
|
|
const np = h("button","am-btn", T("amNewPw"));
|
|
np.addEventListener("click", async ()=>{
|
|
try{
|
|
const r = await api("/admin/users/"+u.id, {method:"PATCH", body:{generatePassword:true}});
|
|
msg(`${T("amPwSet")} ${r.password}`);
|
|
reload();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
row.appendChild(np);
|
|
}
|
|
}
|
|
if(u.id !== currentUser.id){
|
|
const del = h("button","am-del","🗑");
|
|
del.title = T("amDelete") || "";
|
|
del.addEventListener("click", async ()=>{
|
|
if(!confirm(T("amConfirmDel"))) return;
|
|
try{ await api("/admin/users/"+u.id, {method:"DELETE"}); reload(); }
|
|
catch(e){ msg(e.message); }
|
|
});
|
|
row.appendChild(del);
|
|
}
|
|
return row;
|
|
}
|
|
|
|
function addForm(role){
|
|
const f = h("div","am-add");
|
|
const name = h("input","am-inp");
|
|
name.placeholder = T("amUserPh");
|
|
name.addEventListener("keydown", ev=>ev.stopPropagation());
|
|
f.appendChild(name);
|
|
let cs = null;
|
|
if(role==="pupil"){ cs = classSel(""); f.appendChild(cs); }
|
|
let pwInp = null;
|
|
if(role!=="pupil"){
|
|
/* optioneel: meteen zelf een wachtwoord instellen i.p.v. een koppelcode
|
|
(die de gebruiker normaal zelf gebruikt om een wachtwoord te kiezen) */
|
|
pwInp = h("input","am-inp am-pwinp");
|
|
pwInp.type = "password";
|
|
pwInp.autocomplete = "new-password";
|
|
pwInp.placeholder = T("amPwOptional");
|
|
pwInp.addEventListener("keydown", ev=>ev.stopPropagation());
|
|
f.appendChild(pwInp);
|
|
const gen = h("button","am-btn","🎲");
|
|
gen.type = "button";
|
|
gen.title = T("amGenerate");
|
|
gen.addEventListener("click", async ()=>{
|
|
try{
|
|
const { password } = await api("/admin/genpw");
|
|
pwInp.type = "text";
|
|
pwInp.value = password;
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
f.appendChild(gen);
|
|
}
|
|
const go = h("button","tbtn", T(role==="pupil" ? "amNewPupil" : role==="teacher" ? "amNewTeacher" : role==="admin" ? "amNewAdmin" : "amNewSuper"));
|
|
go.addEventListener("click", async ()=>{
|
|
const body = { role, username: name.value.trim(), displayName: name.value.trim() };
|
|
if(role==="pupil" && cs && cs.value) body.classId = +cs.value;
|
|
if(pwInp && pwInp.value.trim()) body.password = pwInp.value.trim();
|
|
if(currentUser.role==="super" && role!=="super") body.school = +selSchool;
|
|
try{
|
|
const r = await api("/admin/users", { body });
|
|
msg(role==="pupil"
|
|
? `${T("amPupilMade")} ${r.user.password}`
|
|
: r.user.password
|
|
? `${T("amPwSet")} ${r.user.password}`
|
|
: `${T("amCreated")} ${r.user.linkCode}`);
|
|
name.value = "";
|
|
if(pwInp) pwInp.value = "";
|
|
reload();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
f.appendChild(go);
|
|
return f;
|
|
}
|
|
|
|
function render(){
|
|
modal.innerHTML = "";
|
|
const head = h("div","am-head");
|
|
head.appendChild(h("h2", null, T("beheer")));
|
|
const close = h("button","wclose","✕");
|
|
close.addEventListener("click", ()=>wrap.classList.remove("open"));
|
|
head.appendChild(close);
|
|
modal.appendChild(head);
|
|
|
|
/* super: schoolkiezer + nieuwe school */
|
|
if(currentUser.role==="super"){
|
|
const bar = h("div","am-add");
|
|
const sel = h("select","am-sel");
|
|
sel.appendChild(new Option(T("amAllSchools"), ""));
|
|
SCHOOLS.forEach(s=>sel.appendChild(new Option(s.name, s.id)));
|
|
sel.value = selSchool ?? "";
|
|
sel.addEventListener("change", ()=>{ selSchool = sel.value; reload(); });
|
|
bar.appendChild(sel);
|
|
const inp = h("input","am-inp");
|
|
inp.placeholder = T("amNamePh");
|
|
inp.addEventListener("keydown", ev=>ev.stopPropagation());
|
|
bar.appendChild(inp);
|
|
const add = h("button","tbtn", T("amNewSchool"));
|
|
add.addEventListener("click", async ()=>{
|
|
if(!inp.value.trim()) return;
|
|
try{
|
|
const r = await api("/admin/schools", { body:{ name: inp.value.trim() } });
|
|
selSchool = r.school.id; inp.value = "";
|
|
reload();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
bar.appendChild(add);
|
|
modal.appendChild(bar);
|
|
}
|
|
|
|
modal.appendChild(h("div","am-msg"));
|
|
|
|
/* klassen (admin/super binnen een school) */
|
|
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
|
if(schoolChosen){
|
|
modal.appendChild(h("h3","am-h3", T("amClasses")));
|
|
const cwrapEl = h("div","am-classes");
|
|
CLASSES.forEach(c=>{
|
|
const chip = h("span","am-chip", c.name);
|
|
if(["super","admin"].includes(currentUser.role)){
|
|
const x = h("button","am-chipx","✕");
|
|
x.addEventListener("click", async ()=>{
|
|
try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); }
|
|
catch(e){ msg(e.message); }
|
|
});
|
|
chip.appendChild(x);
|
|
}
|
|
cwrapEl.appendChild(chip);
|
|
});
|
|
modal.appendChild(cwrapEl);
|
|
if(["super","admin"].includes(currentUser.role)){
|
|
const f = h("div","am-add");
|
|
const inp = h("input","am-inp");
|
|
inp.placeholder = T("amNamePh");
|
|
inp.addEventListener("keydown", ev=>ev.stopPropagation());
|
|
f.appendChild(inp);
|
|
const go = h("button","tbtn", T("amNewClass"));
|
|
go.addEventListener("click", async ()=>{
|
|
if(!inp.value.trim()) return;
|
|
try{
|
|
await api("/admin/classes", { body:{ name: inp.value.trim(), school: selSchool ? +selSchool : undefined } });
|
|
reload();
|
|
}catch(e){ msg(e.message); }
|
|
});
|
|
f.appendChild(go);
|
|
modal.appendChild(f);
|
|
}
|
|
}
|
|
|
|
/* gebruikers */
|
|
modal.appendChild(h("h3","am-h3", T("amUsers")));
|
|
const list = h("div","am-list");
|
|
const groups = [["super", T("amSuperGroup")], ["admin", T("roleAdmin")], ["teacher", T("roleTeacher")], ["pupil", T("rolePupil")]];
|
|
groups.forEach(([role, label])=>{
|
|
const us = USERS.filter(u=>u.role===role);
|
|
if(!us.length) return;
|
|
list.appendChild(h("div","am-group", label));
|
|
us.forEach(u=>list.appendChild(userRow(u)));
|
|
});
|
|
modal.appendChild(list);
|
|
|
|
/* toevoegen */
|
|
if(schoolChosen){
|
|
if(["super","admin","teacher"].includes(currentUser.role)) modal.appendChild(addForm("pupil"));
|
|
if(["super","admin"].includes(currentUser.role)){
|
|
modal.appendChild(addForm("teacher"));
|
|
modal.appendChild(addForm("admin"));
|
|
}
|
|
}
|
|
if(currentUser.role==="super") modal.appendChild(addForm("super"));
|
|
}
|
|
})();
|