v0.2.08-beta: instellingen-pagina, vernieuwd inlogscherm, centrale permissiematrix + rolwijziging
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 18s
- Inlogscherm herzien: aparte tabs voor "Inloggen" en "Koppelcode activeren" i.p.v. één doorlopende vorm
- Nieuwe Instellingen-pagina: account-info, wachtwoord wijzigen en uitloggen zijn verplaatst uit het inlogscherm naar een eigen scherm (👤-knop opent nu Instellingen i.p.v. het inlogscherm zodra je bent ingelogd)
- Laadstatus op de inlog-, koppelcode- en wachtwoordknoppen tijdens het versturen; consistente foutmeldingen i.p.v. een losse alert() bij het wijzigen van je eigen wachtwoord
- Centrale, benoemde permissiematrix (nieuw: src/permissions.js + public/js/permissions.js) vervangt de verspreide rol-arrays in src/api.js en de losse rol-checks in admin.js/core.js
- Nieuw: een overkoepelend beheerder (super) kan nu de rol van een stafaccount (groepsleiding/schoolbeheerder/overkoepelend) rechtstreeks in het beheerpaneel wijzigen; leerlingaccounts blijven altijd leerling
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b80da89ab6
commit
b52c45329c
7 changed files with 184 additions and 73 deletions
|
|
@ -51,6 +51,7 @@
|
||||||
.tbtn:hover{background:var(--blue-d);}
|
.tbtn:hover{background:var(--blue-d);}
|
||||||
.tbtn.ghost{background:#e8eef6; color:var(--ink);}
|
.tbtn.ghost{background:#e8eef6; color:var(--ink);}
|
||||||
.tbtn.ghost:hover{background:#dbe5f1;}
|
.tbtn.ghost:hover{background:#dbe5f1;}
|
||||||
|
.tbtn:disabled{opacity:.55; cursor:default;}
|
||||||
#btnSave{min-width:158px; text-align:center;}
|
#btnSave{min-width:158px; text-align:center;}
|
||||||
#btnSave.ok{background:var(--green); color:#fff;}
|
#btnSave.ok{background:var(--green); color:#fff;}
|
||||||
#btnUser.on{background:var(--green); color:#fff;}
|
#btnUser.on{background:var(--green); color:#fff;}
|
||||||
|
|
@ -411,22 +412,21 @@
|
||||||
.gcard .gi{font-size:38px;}
|
.gcard .gi{font-size:38px;}
|
||||||
.gcard .gn{font-weight:800; margin-top:8px; font-size:15px;}
|
.gcard .gn{font-weight:800; margin-top:8px; font-size:15px;}
|
||||||
.gcard .gd{font-size:12.5px; color:var(--muted); margin-top:4px; line-height:1.35;}
|
.gcard .gd{font-size:12.5px; color:var(--muted); margin-top:4px; line-height:1.35;}
|
||||||
/* login modal */
|
/* login + instellingen modals */
|
||||||
#loginModal{width:min(400px,92vw);}
|
#loginModal,#settingsModal{width:min(400px,92vw);}
|
||||||
#loginModal input{
|
#loginModal input,#settingsModal input{
|
||||||
width:100%; border:2px solid #cdd9e8; border-radius:12px; padding:11px 12px;
|
width:100%; border:2px solid #cdd9e8; border-radius:12px; padding:11px 12px;
|
||||||
font:inherit; font-size:16px; outline:none; margin-bottom:10px;
|
font:inherit; font-size:16px; outline:none; margin-bottom:10px;
|
||||||
}
|
}
|
||||||
#loginModal input:focus{border-color:var(--blue);}
|
#loginModal input:focus,#settingsModal input:focus{border-color:var(--blue);}
|
||||||
.lrow{display:flex; gap:8px;}
|
.lrow{display:flex; gap:8px;}
|
||||||
.lrow .tbtn{flex:1;}
|
.lrow .tbtn{flex:1;}
|
||||||
.lrow input{flex:1; min-width:0; margin-bottom:0;}
|
.lrow input{flex:1; min-width:0; margin-bottom:0;}
|
||||||
.lrow .pwtoggle{flex:0 0 auto; white-space:nowrap; padding-left:10px; padding-right:10px;}
|
.lrow .pwtoggle{flex:0 0 auto; white-space:nowrap; padding-left:10px; padding-right:10px;}
|
||||||
#loginMsg{min-height:20px; font-size:13.5px; font-weight:700; color:var(--red); margin:6px 0;}
|
.formMsg{min-height:20px; font-size:13.5px; font-weight:700; color:var(--red); margin:6px 0;}
|
||||||
#loginMsg.ok{color:var(--green);}
|
.formMsg.ok{color:var(--green);}
|
||||||
.guestnote{font-size:13px; color:var(--muted); margin-top:12px; line-height:1.45;}
|
.guestnote{font-size:13px; color:var(--muted); margin-top:12px; line-height:1.45;}
|
||||||
#loggedBox{display:none;}
|
#settingsModal .hello{font-size:18px; font-weight:800; margin-bottom:14px;}
|
||||||
#loggedBox .hello{font-size:18px; font-weight:800; margin-bottom:14px;}
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
Letter game widget
|
Letter game widget
|
||||||
==========================================================*/
|
==========================================================*/
|
||||||
|
|
@ -966,10 +966,9 @@
|
||||||
width:100%; border:2px solid #cdd9e8; border-radius:12px; padding:11px 12px;
|
width:100%; border:2px solid #cdd9e8; border-radius:12px; padding:11px 12px;
|
||||||
font:inherit; font-size:15px; outline:none; margin-bottom:10px; background:#fff;
|
font:inherit; font-size:15px; outline:none; margin-bottom:10px; background:#fff;
|
||||||
}
|
}
|
||||||
.lidiv{height:1.5px; background:#e3eaf3; margin:14px 0;}
|
|
||||||
.lisub{font-size:13px; font-weight:700; color:var(--muted); margin-bottom:8px;}
|
.lisub{font-size:13px; font-weight:700; color:var(--muted); margin-bottom:8px;}
|
||||||
#ownPwRow{margin-bottom:10px;}
|
#ownPwBlock .lrow{margin-bottom:10px;}
|
||||||
#ownPwRow input{
|
#ownPwBlock input{
|
||||||
flex:1; min-width:0; border:2px solid #cdd9e8; border-radius:12px;
|
flex:1; min-width:0; border:2px solid #cdd9e8; border-radius:12px;
|
||||||
padding:10px 12px; font:inherit; font-size:15px; outline:none;
|
padding:10px 12px; font:inherit; font-size:15px; outline:none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,16 +69,21 @@
|
||||||
|
|
||||||
<div class="modalwrap" id="loginWrap">
|
<div class="modalwrap" id="loginWrap">
|
||||||
<div class="modal" id="loginModal">
|
<div class="modal" id="loginModal">
|
||||||
|
<h2 data-i18n="login"></h2>
|
||||||
|
<div class="ed-tabs">
|
||||||
|
<button type="button" class="active" data-tab="login" data-i18n="tabLogin"></button>
|
||||||
|
<button type="button" data-tab="link" data-i18n="tabLinkCode"></button>
|
||||||
|
</div>
|
||||||
<div id="loginBox">
|
<div id="loginBox">
|
||||||
<h2 data-i18n="login"></h2>
|
|
||||||
<select id="liSchool"></select>
|
<select id="liSchool"></select>
|
||||||
<input type="text" id="liName" maxlength="30" autocomplete="username">
|
<input type="text" id="liName" maxlength="30" autocomplete="username">
|
||||||
<input type="password" id="liPass" maxlength="64" autocomplete="current-password">
|
<input type="password" id="liPass" maxlength="64" autocomplete="current-password">
|
||||||
<div id="loginMsg"></div>
|
<div id="loginMsg" class="formMsg"></div>
|
||||||
<div class="lrow">
|
<div class="lrow">
|
||||||
<button class="tbtn" id="btnLogin" data-i18n="loginBtn"></button>
|
<button class="tbtn" id="btnLogin" data-i18n="loginBtn"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lidiv"></div>
|
</div>
|
||||||
|
<div id="linkBox" style="display:none;">
|
||||||
<div class="lisub" data-i18n="linkIntro"></div>
|
<div class="lisub" data-i18n="linkIntro"></div>
|
||||||
<input type="text" id="liCode" maxlength="9" style="text-transform:uppercase;">
|
<input type="text" id="liCode" maxlength="9" style="text-transform:uppercase;">
|
||||||
<div class="lrow">
|
<div class="lrow">
|
||||||
|
|
@ -89,11 +94,17 @@
|
||||||
<div class="lrow">
|
<div class="lrow">
|
||||||
<button class="tbtn ghost" id="btnLink" data-i18n="linkBtn"></button>
|
<button class="tbtn ghost" id="btnLink" data-i18n="linkBtn"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="guestnote" data-i18n="guestNote"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="loggedBox">
|
<div class="guestnote" data-i18n="guestNote"></div>
|
||||||
<div class="hello" id="helloLbl"></div>
|
</div>
|
||||||
<div class="lrow" id="ownPwRow">
|
</div>
|
||||||
|
<div class="modalwrap" id="settingsWrap">
|
||||||
|
<div class="modal" id="settingsModal">
|
||||||
|
<h2 data-i18n="settingsTitle"></h2>
|
||||||
|
<div class="hello" id="helloLbl"></div>
|
||||||
|
<div id="settingsMsg" class="formMsg"></div>
|
||||||
|
<div id="ownPwBlock">
|
||||||
|
<div class="lrow">
|
||||||
<input type="password" id="ownPw" maxlength="64" autocomplete="new-password">
|
<input type="password" id="ownPw" maxlength="64" autocomplete="new-password">
|
||||||
<button class="tbtn ghost pwtoggle" type="button" id="btnOwnPwToggle" data-i18n="pwShow"></button>
|
<button class="tbtn ghost pwtoggle" type="button" id="btnOwnPwToggle" data-i18n="pwShow"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,11 +112,11 @@
|
||||||
<input type="password" id="ownPw2" maxlength="64" autocomplete="new-password">
|
<input type="password" id="ownPw2" maxlength="64" autocomplete="new-password">
|
||||||
<button class="tbtn ghost" id="btnOwnPw" data-i18n="pwChange"></button>
|
<button class="tbtn ghost" id="btnOwnPw" data-i18n="pwChange"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lrow">
|
|
||||||
<button class="tbtn ghost" id="btnLogout" data-i18n="logout"></button>
|
|
||||||
</div>
|
|
||||||
<div class="guestnote" data-i18n="storedNote"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="lrow">
|
||||||
|
<button class="tbtn ghost" id="btnLogout" data-i18n="logout"></button>
|
||||||
|
</div>
|
||||||
|
<div class="guestnote" data-i18n="storedNote"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modalwrap" id="adminWrap">
|
<div class="modalwrap" id="adminWrap">
|
||||||
|
|
@ -113,6 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/core.js"></script>
|
<script src="js/core.js"></script>
|
||||||
|
<script src="js/permissions.js"></script>
|
||||||
<script src="js/data.js"></script>
|
<script src="js/data.js"></script>
|
||||||
<script src="js/widgets/letters.js"></script>
|
<script src="js/widgets/letters.js"></script>
|
||||||
<script src="js/widgets/write.js"></script>
|
<script src="js/widgets/write.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
btn.addEventListener("click", async ()=>{
|
btn.addEventListener("click", async ()=>{
|
||||||
if(!currentUser || currentUser.role==="pupil") return;
|
if(!can("admin.access")) return;
|
||||||
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
selSchool = currentUser.role==="super" ? (selSchool ?? "") : currentUser.schoolId;
|
||||||
await reload();
|
await reload();
|
||||||
wrap.classList.add("open");
|
wrap.classList.add("open");
|
||||||
|
|
@ -84,7 +84,19 @@
|
||||||
tussen de andere rij-elementen te wrappen */
|
tussen de andere rij-elementen te wrappen */
|
||||||
const actions = h("div","am-actions");
|
const actions = h("div","am-actions");
|
||||||
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
|
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" && can("users.role.change") && u.id!==currentUser.id){
|
||||||
|
/* super mag de rol van andere stafaccounts wijzigen - leerlingen blijven altijd leerling */
|
||||||
|
const rs = h("select","am-sel");
|
||||||
|
["teacher","admin","super"].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(); }
|
||||||
|
catch(e){ msg(e.message); }
|
||||||
|
});
|
||||||
|
row.appendChild(rs);
|
||||||
|
}else{
|
||||||
|
row.appendChild(h("span","am-role", T(roleKey(u.role))));
|
||||||
|
}
|
||||||
if(u.role==="pupil"){
|
if(u.role==="pupil"){
|
||||||
/* klas (uitwisselen met andere klassen) */
|
/* klas (uitwisselen met andere klassen) */
|
||||||
const cs = classSel(u.classId);
|
const cs = classSel(u.classId);
|
||||||
|
|
@ -130,7 +142,7 @@
|
||||||
}else if(u.pending){
|
}else if(u.pending){
|
||||||
row.appendChild(h("span","am-code", T("amPending")));
|
row.appendChild(h("span","am-code", T("amPending")));
|
||||||
}
|
}
|
||||||
if(["super","admin"].includes(currentUser.role) && u.role!=="pupil" && u.id!==currentUser.id){
|
if(can("users.staffCredentials") && u.role!=="pupil" && u.id!==currentUser.id){
|
||||||
const nc = h("button","am-btn", T("amNewCode"));
|
const nc = h("button","am-btn", T("amNewCode"));
|
||||||
nc.addEventListener("click", async ()=>{
|
nc.addEventListener("click", async ()=>{
|
||||||
try{
|
try{
|
||||||
|
|
@ -293,7 +305,7 @@
|
||||||
|
|
||||||
/* 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);
|
const canResetPw = can("schools.resetPasswords");
|
||||||
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");
|
||||||
|
|
@ -369,14 +381,11 @@
|
||||||
});
|
});
|
||||||
modal.appendChild(list);
|
modal.appendChild(list);
|
||||||
|
|
||||||
/* toevoegen */
|
/* toevoegen - welke rollen deze gebruiker mag aanmaken (CREATABLE_ROLES) */
|
||||||
|
const creatable = CREATABLE_ROLES[currentUser.role];
|
||||||
if(schoolChosen){
|
if(schoolChosen){
|
||||||
if(["super","admin","teacher"].includes(currentUser.role)) modal.appendChild(addForm("pupil"));
|
["pupil","teacher","admin"].forEach(r=>{ if(creatable.includes(r)) modal.appendChild(addForm(r)); });
|
||||||
if(["super","admin"].includes(currentUser.role)){
|
|
||||||
modal.appendChild(addForm("teacher"));
|
|
||||||
modal.appendChild(addForm("admin"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if(currentUser.role==="super") modal.appendChild(addForm("super"));
|
if(creatable.includes("super")) modal.appendChild(addForm("super"));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,8 @@ const I18N = {
|
||||||
tmDigits:"Cijfers", tmVisual:"Kijkklok", tmBoth:"Allebei", tmSound:"Geluid aan/uit",
|
tmDigits:"Cijfers", tmVisual:"Kijkklok", tmBoth:"Allebei", tmSound:"Geluid aan/uit",
|
||||||
tmHint:"Sleep aan de rode schijf om de tijd in te stellen.",
|
tmHint:"Sleep aan de rode schijf om de tijd in te stellen.",
|
||||||
login:"Inloggen", loginBtn:"Inloggen", registerBtn:"Account maken", logout:"Uitloggen",
|
login:"Inloggen", loginBtn:"Inloggen", registerBtn:"Account maken", logout:"Uitloggen",
|
||||||
|
settingsTitle:"Instellingen", tabLogin:"Inloggen", tabLinkCode:"Koppelcode", busy:"Bezig…",
|
||||||
|
pwChanged:"Wachtwoord gewijzigd ✓",
|
||||||
namePh:"naam", passPh:"wachtwoord",
|
namePh:"naam", passPh:"wachtwoord",
|
||||||
guestNote:"Zonder inloggen wordt niets bewaard. Log in met je schoolaccount om alles op de server te bewaren.",
|
guestNote:"Zonder inloggen wordt niets bewaard. Log in met je schoolaccount om alles op de server te bewaren.",
|
||||||
storedNote:"Je werk staat op de school-server en is op elk digibord beschikbaar.",
|
storedNote:"Je werk staat op de school-server en is op elk digibord beschikbaar.",
|
||||||
|
|
@ -231,6 +233,8 @@ const I18N = {
|
||||||
tmDigits:"Digits", tmVisual:"Visual clock", tmBoth:"Both", tmSound:"Sound on/off",
|
tmDigits:"Digits", tmVisual:"Visual clock", tmBoth:"Both", tmSound:"Sound on/off",
|
||||||
tmHint:"Drag the red disc to set the time.",
|
tmHint:"Drag the red disc to set the time.",
|
||||||
login:"Log in", loginBtn:"Log in", registerBtn:"Create account", logout:"Log out",
|
login:"Log in", loginBtn:"Log in", registerBtn:"Create account", logout:"Log out",
|
||||||
|
settingsTitle:"Settings", tabLogin:"Log in", tabLinkCode:"Link code", busy:"Please wait…",
|
||||||
|
pwChanged:"Password changed ✓",
|
||||||
namePh:"name", passPh:"password",
|
namePh:"name", passPh:"password",
|
||||||
guestNote:"Without logging in nothing is saved. Log in with your school account to keep everything on the server.",
|
guestNote:"Without logging in nothing is saved. Log in with your school account to keep everything on the server.",
|
||||||
storedNote:"Your work is stored on the school server and available on any board.",
|
storedNote:"Your work is stored on the school server and available on any board.",
|
||||||
|
|
@ -360,21 +364,30 @@ function resetToGuest(){
|
||||||
/* login UI */
|
/* login UI */
|
||||||
const loginWrap = document.getElementById("loginWrap");
|
const loginWrap = document.getElementById("loginWrap");
|
||||||
const loginMsg = document.getElementById("loginMsg");
|
const loginMsg = document.getElementById("loginMsg");
|
||||||
|
const settingsWrap = document.getElementById("settingsWrap");
|
||||||
|
const settingsMsg = document.getElementById("settingsMsg");
|
||||||
const roleKey = r => r==="super" ? "roleSuper" : r==="admin" ? "roleAdmin" : r==="teacher" ? "roleTeacher" : "rolePupil";
|
const roleKey = r => r==="super" ? "roleSuper" : r==="admin" ? "roleAdmin" : r==="teacher" ? "roleTeacher" : "rolePupil";
|
||||||
function updateUserUI(){
|
function updateUserUI(){
|
||||||
document.getElementById("userLbl").textContent = currentUser ? currentUser.displayName : T("login");
|
document.getElementById("userLbl").textContent = currentUser ? currentUser.displayName : T("login");
|
||||||
document.getElementById("btnUser").classList.toggle("on", !!currentUser);
|
document.getElementById("btnUser").classList.toggle("on", !!currentUser);
|
||||||
document.getElementById("loginBox").style.display = currentUser ? "none" : "block";
|
|
||||||
document.getElementById("loggedBox").style.display = currentUser ? "block" : "none";
|
|
||||||
const adminBtn = document.getElementById("btnAdmin");
|
const adminBtn = document.getElementById("btnAdmin");
|
||||||
adminBtn.style.display = currentUser && currentUser.role !== "pupil" ? "" : "none";
|
adminBtn.style.display = can("admin.access") ? "" : "none";
|
||||||
adminBtn.title = T("beheer");
|
adminBtn.title = T("beheer");
|
||||||
if(currentUser){
|
if(currentUser){
|
||||||
document.getElementById("helloLbl").textContent =
|
document.getElementById("helloLbl").textContent =
|
||||||
`${T("hello")} ${currentUser.displayName}! 👋 · ${T(roleKey(currentUser.role))}`;
|
`${T("hello")} ${currentUser.displayName}! 👋 · ${T(roleKey(currentUser.role))}`;
|
||||||
document.getElementById("ownPwRow").style.display = currentUser.role==="pupil" ? "none" : "flex";
|
document.getElementById("ownPwBlock").style.display = can("password.own.change") ? "block" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* login-modal tabs: "inloggen" vs "koppelcode activeren" */
|
||||||
|
function switchLoginTab(tab){
|
||||||
|
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>b.classList.toggle("active", b.dataset.tab===tab));
|
||||||
|
document.getElementById("loginBox").style.display = tab==="login" ? "block" : "none";
|
||||||
|
document.getElementById("linkBox").style.display = tab==="link" ? "block" : "none";
|
||||||
|
}
|
||||||
|
document.querySelectorAll("#loginModal .ed-tabs button").forEach(b=>{
|
||||||
|
b.addEventListener("click", ()=>switchLoginTab(b.dataset.tab));
|
||||||
|
});
|
||||||
async function loadSchools(){
|
async function loadSchools(){
|
||||||
const sel = document.getElementById("liSchool");
|
const sel = document.getElementById("liSchool");
|
||||||
try{
|
try{
|
||||||
|
|
@ -384,11 +397,18 @@ async function loadSchools(){
|
||||||
}catch(e){ sel.innerHTML = `<option value="">${T("schoolSel")}</option>`; }
|
}catch(e){ sel.innerHTML = `<option value="">${T("schoolSel")}</option>`; }
|
||||||
}
|
}
|
||||||
document.getElementById("btnUser").addEventListener("click", ()=>{
|
document.getElementById("btnUser").addEventListener("click", ()=>{
|
||||||
|
if(currentUser){
|
||||||
|
settingsMsg.textContent = ""; settingsMsg.classList.remove("ok");
|
||||||
|
settingsWrap.classList.add("open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
loginMsg.textContent = ""; loginMsg.classList.remove("ok");
|
loginMsg.textContent = ""; loginMsg.classList.remove("ok");
|
||||||
|
switchLoginTab("login");
|
||||||
loadSchools();
|
loadSchools();
|
||||||
loginWrap.classList.add("open");
|
loginWrap.classList.add("open");
|
||||||
});
|
});
|
||||||
loginWrap.addEventListener("click", e=>{ if(e.target===loginWrap) loginWrap.classList.remove("open"); });
|
loginWrap.addEventListener("click", e=>{ if(e.target===loginWrap) loginWrap.classList.remove("open"); });
|
||||||
|
settingsWrap.addEventListener("click", e=>{ if(e.target===settingsWrap) settingsWrap.classList.remove("open"); });
|
||||||
|
|
||||||
async function afterLogin(res){
|
async function afterLogin(res){
|
||||||
TOKEN = res.token;
|
TOKEN = res.token;
|
||||||
|
|
@ -398,15 +418,25 @@ async function afterLogin(res){
|
||||||
loginWrap.classList.remove("open");
|
loginWrap.classList.remove("open");
|
||||||
await hydrateFromServer();
|
await hydrateFromServer();
|
||||||
}
|
}
|
||||||
|
/* laadstatus op een knop tijdens een async actie, altijd hersteld na afloop */
|
||||||
|
async function withBusy(btn, fn){
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.disabled = true; btn.textContent = T("busy");
|
||||||
|
try{ await fn(); }
|
||||||
|
finally{ btn.disabled = false; btn.textContent = orig; }
|
||||||
|
}
|
||||||
document.getElementById("btnLogin").addEventListener("click", async ()=>{
|
document.getElementById("btnLogin").addEventListener("click", async ()=>{
|
||||||
|
const btn = document.getElementById("btnLogin");
|
||||||
const schoolId = document.getElementById("liSchool").value || null;
|
const schoolId = document.getElementById("liSchool").value || null;
|
||||||
const username = document.getElementById("liName").value.trim();
|
const username = document.getElementById("liName").value.trim();
|
||||||
const password = document.getElementById("liPass").value;
|
const password = document.getElementById("liPass").value;
|
||||||
loginMsg.classList.remove("ok");
|
loginMsg.classList.remove("ok");
|
||||||
try{
|
await withBusy(btn, async ()=>{
|
||||||
await afterLogin(await api("/auth/login", { body:{ schoolId: schoolId ? +schoolId : null, username, password } }));
|
try{
|
||||||
document.getElementById("liPass").value = "";
|
await afterLogin(await api("/auth/login", { body:{ schoolId: schoolId ? +schoolId : null, username, password } }));
|
||||||
}catch(e){ loginMsg.textContent = e.message || T("errLogin"); }
|
document.getElementById("liPass").value = "";
|
||||||
|
}catch(e){ loginMsg.textContent = e.message || T("errLogin"); }
|
||||||
|
});
|
||||||
});
|
});
|
||||||
document.getElementById("liPass").addEventListener("keydown", e=>{
|
document.getElementById("liPass").addEventListener("keydown", e=>{
|
||||||
if(e.key==="Enter") document.getElementById("btnLogin").click();
|
if(e.key==="Enter") document.getElementById("btnLogin").click();
|
||||||
|
|
@ -426,33 +456,42 @@ pwToggle("btnOwnPwToggle", "ownPw", "ownPw2");
|
||||||
|
|
||||||
/* account activeren met een koppelcode */
|
/* account activeren met een koppelcode */
|
||||||
document.getElementById("btnLink").addEventListener("click", async ()=>{
|
document.getElementById("btnLink").addEventListener("click", async ()=>{
|
||||||
|
const btn = document.getElementById("btnLink");
|
||||||
const code = document.getElementById("liCode").value.trim().toUpperCase();
|
const code = document.getElementById("liCode").value.trim().toUpperCase();
|
||||||
const password = document.getElementById("liPassNew").value;
|
const password = document.getElementById("liPassNew").value;
|
||||||
const password2 = document.getElementById("liPassNew2").value;
|
const password2 = document.getElementById("liPassNew2").value;
|
||||||
loginMsg.classList.remove("ok");
|
loginMsg.classList.remove("ok");
|
||||||
if(password !== password2){ loginMsg.textContent = T("errPwMatch"); return; }
|
if(password !== password2){ loginMsg.textContent = T("errPwMatch"); return; }
|
||||||
try{
|
await withBusy(btn, async ()=>{
|
||||||
await afterLogin(await api("/auth/link", { body:{ code, password } }));
|
try{
|
||||||
document.getElementById("liCode").value = "";
|
await afterLogin(await api("/auth/link", { body:{ code, password } }));
|
||||||
document.getElementById("liPassNew").value = "";
|
document.getElementById("liCode").value = "";
|
||||||
document.getElementById("liPassNew2").value = "";
|
document.getElementById("liPassNew").value = "";
|
||||||
}catch(e){ loginMsg.textContent = e.message; }
|
document.getElementById("liPassNew2").value = "";
|
||||||
|
}catch(e){ loginMsg.textContent = e.message; }
|
||||||
|
});
|
||||||
});
|
});
|
||||||
document.getElementById("btnLogout").addEventListener("click", async ()=>{
|
document.getElementById("btnLogout").addEventListener("click", async ()=>{
|
||||||
try{ await api("/auth/logout", { method:"POST", body:{} }); }catch(e){}
|
try{ await api("/auth/logout", { method:"POST", body:{} }); }catch(e){}
|
||||||
resetToGuest();
|
resetToGuest();
|
||||||
loginWrap.classList.remove("open");
|
loginWrap.classList.remove("open");
|
||||||
|
settingsWrap.classList.remove("open");
|
||||||
});
|
});
|
||||||
/* eigen wachtwoord wijzigen (niet voor leerlingen) */
|
/* eigen wachtwoord wijzigen (niet voor leerlingen) */
|
||||||
document.getElementById("btnOwnPw").addEventListener("click", async ()=>{
|
document.getElementById("btnOwnPw").addEventListener("click", async ()=>{
|
||||||
|
const btn = document.getElementById("btnOwnPw");
|
||||||
const pw = document.getElementById("ownPw").value;
|
const pw = document.getElementById("ownPw").value;
|
||||||
const pw2 = document.getElementById("ownPw2").value;
|
const pw2 = document.getElementById("ownPw2").value;
|
||||||
if(pw !== pw2){ alert(T("errPwMatch")); return; }
|
settingsMsg.classList.remove("ok");
|
||||||
try{
|
if(pw !== pw2){ settingsMsg.textContent = T("errPwMatch"); return; }
|
||||||
await api("/auth/password", { method:"PATCH", body:{ password: pw } });
|
await withBusy(btn, async ()=>{
|
||||||
document.getElementById("ownPw").value = "";
|
try{
|
||||||
document.getElementById("ownPw2").value = "";
|
await api("/auth/password", { method:"PATCH", body:{ password: pw } });
|
||||||
}catch(e){ alert(e.message); }
|
document.getElementById("ownPw").value = "";
|
||||||
|
document.getElementById("ownPw2").value = "";
|
||||||
|
settingsMsg.textContent = T("pwChanged"); settingsMsg.classList.add("ok");
|
||||||
|
}catch(e){ settingsMsg.textContent = e.message; }
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* save board */
|
/* save board */
|
||||||
|
|
|
||||||
26
public/js/permissions.js
Normal file
26
public/js/permissions.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* teach - centrale permissiematrix voor de 4 vaste rollen (super/admin/teacher/pupil).
|
||||||
|
Gespiegeld in src/permissions.js (geen bundler in dit project, dus twee bestanden
|
||||||
|
die dezelfde waarden hand-in-hand bijhouden). */
|
||||||
|
const PERMISSIONS = {
|
||||||
|
'admin.access': ['super', 'admin', 'teacher'],
|
||||||
|
'password.own.change': ['super', 'admin', 'teacher'],
|
||||||
|
'schools.manage': ['super'],
|
||||||
|
'schools.resetPasswords': ['super', 'admin'],
|
||||||
|
'classes.view': ['super', 'admin', 'teacher'],
|
||||||
|
'classes.manage': ['super', 'admin'],
|
||||||
|
'users.manage': ['super', 'admin', 'teacher'],
|
||||||
|
'users.staffCredentials': ['super', 'admin'],
|
||||||
|
'users.genpw': ['super', 'admin', 'teacher'],
|
||||||
|
'users.role.change': ['super'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const CREATABLE_ROLES = {
|
||||||
|
super: ['super', 'admin', 'teacher', 'pupil'],
|
||||||
|
admin: ['admin', 'teacher', 'pupil'],
|
||||||
|
teacher: ['pupil'],
|
||||||
|
};
|
||||||
|
|
||||||
|
/* can(action) gebruikt de ingelogde gebruiker (currentUser, uit core.js) */
|
||||||
|
function can(action){
|
||||||
|
return !!currentUser && (PERMISSIONS[action] || []).includes(currentUser.role);
|
||||||
|
}
|
||||||
40
src/api.js
40
src/api.js
|
|
@ -13,6 +13,7 @@ import {
|
||||||
hashPassword, verifyPassword, createSession, userFromRequest,
|
hashPassword, verifyPassword, createSession, userFromRequest,
|
||||||
newLinkCode, pupilPassword, staffPassword, checkPassword, publicUser,
|
newLinkCode, pupilPassword, staffPassword, checkPassword, publicUser,
|
||||||
} from './auth.js';
|
} from './auth.js';
|
||||||
|
import { PERMISSIONS, CREATABLE_ROLES, can } from './permissions.js';
|
||||||
|
|
||||||
export default async function api(app) {
|
export default async function api(app) {
|
||||||
const pool = app.pg;
|
const pool = app.pg;
|
||||||
|
|
@ -93,7 +94,7 @@ export default async function api(app) {
|
||||||
|
|
||||||
// Eigen wachtwoord wijzigen - niet voor leerlingen
|
// Eigen wachtwoord wijzigen - niet voor leerlingen
|
||||||
app.patch('/auth/password', async (req, reply) => {
|
app.patch('/auth/password', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, PERMISSIONS['password.own.change']);
|
||||||
const { password } = req.body ?? {};
|
const { password } = req.body ?? {};
|
||||||
const err = checkPassword(password, req.user.role);
|
const err = checkPassword(password, req.user.role);
|
||||||
if (err) return fail(reply, 400, err);
|
if (err) return fail(reply, 400, err);
|
||||||
|
|
@ -116,14 +117,14 @@ export default async function api(app) {
|
||||||
|
|
||||||
// ---- scholen (super) ---------------------------------------------------------
|
// ---- scholen (super) ---------------------------------------------------------
|
||||||
app.post('/admin/schools', async (req, reply) => {
|
app.post('/admin/schools', async (req, reply) => {
|
||||||
need(req, reply, ['super']);
|
need(req, reply, PERMISSIONS['schools.manage']);
|
||||||
const { name } = req.body ?? {};
|
const { name } = req.body ?? {};
|
||||||
if (!name) return fail(reply, 400, 'naam verplicht');
|
if (!name) return fail(reply, 400, 'naam verplicht');
|
||||||
const r = await pool.query('INSERT INTO schools (name) VALUES ($1) RETURNING id, name', [name.trim()]);
|
const r = await pool.query('INSERT INTO schools (name) VALUES ($1) RETURNING id, name', [name.trim()]);
|
||||||
return { school: { id: Number(r.rows[0].id), name: r.rows[0].name } };
|
return { school: { id: Number(r.rows[0].id), name: r.rows[0].name } };
|
||||||
});
|
});
|
||||||
app.delete('/admin/schools/:id', async (req, reply) => {
|
app.delete('/admin/schools/:id', async (req, reply) => {
|
||||||
need(req, reply, ['super']);
|
need(req, reply, PERMISSIONS['schools.manage']);
|
||||||
await pool.query('DELETE FROM schools WHERE id = $1', [req.params.id]);
|
await pool.query('DELETE FROM schools WHERE id = $1', [req.params.id]);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
@ -131,7 +132,7 @@ export default async function api(app) {
|
||||||
// Multireset: alle leerlingwachtwoorden van een hele school in één keer
|
// Multireset: alle leerlingwachtwoorden van een hele school in één keer
|
||||||
// vernieuwen. Alleen schoolbeheerder (eigen school) en overkoepelend beheerder.
|
// vernieuwen. Alleen schoolbeheerder (eigen school) en overkoepelend beheerder.
|
||||||
app.post('/admin/schools/:id/reset-passwords', async (req, reply) => {
|
app.post('/admin/schools/:id/reset-passwords', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin']);
|
need(req, reply, PERMISSIONS['schools.resetPasswords']);
|
||||||
const schoolId = req.params.id;
|
const schoolId = req.params.id;
|
||||||
if (req.user.role !== 'super' && Number(schoolId) !== Number(req.user.school_id))
|
if (req.user.role !== 'super' && Number(schoolId) !== Number(req.user.school_id))
|
||||||
return fail(reply, 403, 'geen rechten');
|
return fail(reply, 403, 'geen rechten');
|
||||||
|
|
@ -149,7 +150,7 @@ export default async function api(app) {
|
||||||
|
|
||||||
// ---- 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, PERMISSIONS['classes.view']);
|
||||||
const schoolId = req.user.role === 'super' ? req.query.school : req.user.school_id;
|
const schoolId = req.user.role === 'super' ? req.query.school : req.user.school_id;
|
||||||
if (!schoolId) return { classes: [] };
|
if (!schoolId) return { classes: [] };
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
|
|
@ -160,7 +161,7 @@ export default async function api(app) {
|
||||||
return { classes: r.rows.map((c) => ({ id: Number(c.id), name: c.name, teacherIds: c.teacher_ids.map(Number) })) };
|
return { classes: r.rows.map((c) => ({ id: Number(c.id), name: c.name, teacherIds: c.teacher_ids.map(Number) })) };
|
||||||
});
|
});
|
||||||
app.post('/admin/classes', async (req, reply) => {
|
app.post('/admin/classes', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin']);
|
need(req, reply, PERMISSIONS['classes.manage']);
|
||||||
const { name, school } = req.body ?? {};
|
const { name, school } = req.body ?? {};
|
||||||
const schoolId = req.user.role === 'super' ? school : req.user.school_id;
|
const schoolId = req.user.role === 'super' ? school : req.user.school_id;
|
||||||
if (!name || !schoolId) return fail(reply, 400, 'naam en school verplicht');
|
if (!name || !schoolId) return fail(reply, 400, 'naam en school verplicht');
|
||||||
|
|
@ -168,7 +169,7 @@ export default async function api(app) {
|
||||||
return { class: { id: Number(r.rows[0].id), name: r.rows[0].name } };
|
return { class: { id: Number(r.rows[0].id), name: r.rows[0].name } };
|
||||||
});
|
});
|
||||||
app.delete('/admin/classes/:id', async (req, reply) => {
|
app.delete('/admin/classes/:id', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin']);
|
need(req, reply, PERMISSIONS['classes.manage']);
|
||||||
const r = await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id]);
|
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 (!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.role !== 'super' && Number(r.rows[0].school_id) !== Number(req.user.school_id))
|
||||||
|
|
@ -178,7 +179,7 @@ export default async function api(app) {
|
||||||
});
|
});
|
||||||
// groepsleiding aan klas koppelen/loskoppelen
|
// groepsleiding aan klas koppelen/loskoppelen
|
||||||
app.post('/admin/classes/:id/teachers', async (req, reply) => {
|
app.post('/admin/classes/:id/teachers', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin']);
|
need(req, reply, PERMISSIONS['classes.manage']);
|
||||||
const { userId, remove } = req.body ?? {};
|
const { userId, remove } = req.body ?? {};
|
||||||
if (remove) await pool.query('DELETE FROM class_teachers WHERE class_id = $1 AND user_id = $2', [req.params.id, userId]);
|
if (remove) await pool.query('DELETE FROM class_teachers WHERE class_id = $1 AND user_id = $2', [req.params.id, userId]);
|
||||||
else await pool.query('INSERT INTO class_teachers (class_id, user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [req.params.id, userId]);
|
else await pool.query('INSERT INTO class_teachers (class_id, user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [req.params.id, userId]);
|
||||||
|
|
@ -189,7 +190,7 @@ export default async function api(app) {
|
||||||
// Alleen schoolbeheerder (eigen school) en overkoepelend beheerder - niet
|
// Alleen schoolbeheerder (eigen school) en overkoepelend beheerder - niet
|
||||||
// de groepsleiding, om per ongeluk een hele klas resetten te voorkomen.
|
// de groepsleiding, om per ongeluk een hele klas resetten te voorkomen.
|
||||||
app.post('/admin/classes/:id/reset-passwords', async (req, reply) => {
|
app.post('/admin/classes/:id/reset-passwords', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin']);
|
need(req, reply, PERMISSIONS['classes.manage']);
|
||||||
const r = await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id]);
|
const r = await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id]);
|
||||||
const c = r.rows[0];
|
const c = r.rows[0];
|
||||||
if (!c) return fail(reply, 404, 'klas onbekend');
|
if (!c) return fail(reply, 404, 'klas onbekend');
|
||||||
|
|
@ -209,7 +210,7 @@ export default async function api(app) {
|
||||||
|
|
||||||
// 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, PERMISSIONS['users.genpw']);
|
||||||
const role = req.query.role === 'pupil' ? 'pupil' : 'staff';
|
const role = req.query.role === 'pupil' ? 'pupil' : 'staff';
|
||||||
return { password: role === 'pupil' ? pupilPassword() : staffPassword() };
|
return { password: role === 'pupil' ? pupilPassword() : staffPassword() };
|
||||||
});
|
});
|
||||||
|
|
@ -217,7 +218,7 @@ export default async function api(app) {
|
||||||
// ---- gebruikers ---------------------------------------------------------------
|
// ---- gebruikers ---------------------------------------------------------------
|
||||||
// Lijst: super ziet alles (optioneel per school), admin/teacher de eigen school.
|
// Lijst: super ziet alles (optioneel per school), admin/teacher de eigen school.
|
||||||
app.get('/admin/users', async (req, reply) => {
|
app.get('/admin/users', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, PERMISSIONS['users.manage']);
|
||||||
let rows;
|
let rows;
|
||||||
if (req.user.role === 'super') {
|
if (req.user.role === 'super') {
|
||||||
rows = (req.query.school
|
rows = (req.query.school
|
||||||
|
|
@ -226,7 +227,7 @@ export default async function api(app) {
|
||||||
} else {
|
} else {
|
||||||
rows = (await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.user.school_id])).rows;
|
rows = (await pool.query('SELECT * FROM users WHERE school_id = $1 ORDER BY role, lower(username)', [req.user.school_id])).rows;
|
||||||
}
|
}
|
||||||
const canSeePw = ['super', 'admin', 'teacher'].includes(req.user.role);
|
const canSeePw = can(req.user.role, 'users.manage');
|
||||||
return { users: rows.map((u) => ({
|
return { users: rows.map((u) => ({
|
||||||
...publicUser(u),
|
...publicUser(u),
|
||||||
linkCode: u.link_code || null,
|
linkCode: u.link_code || null,
|
||||||
|
|
@ -240,11 +241,10 @@ export default async function api(app) {
|
||||||
// Aanmaken. super: alles (ook supers). admin: admin/teacher/pupil in eigen school.
|
// Aanmaken. super: alles (ook supers). admin: admin/teacher/pupil in eigen school.
|
||||||
// teacher: alleen leerlingen in de eigen school.
|
// teacher: alleen leerlingen in de eigen school.
|
||||||
app.post('/admin/users', async (req, reply) => {
|
app.post('/admin/users', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, PERMISSIONS['users.manage']);
|
||||||
const b = req.body ?? {};
|
const b = req.body ?? {};
|
||||||
const role = b.role;
|
const role = b.role;
|
||||||
const allowed = { super: ['super', 'admin', 'teacher', 'pupil'], admin: ['admin', 'teacher', 'pupil'], teacher: ['pupil'] };
|
if (!CREATABLE_ROLES[req.user.role].includes(role)) return fail(reply, 403, 'geen rechten voor deze rol');
|
||||||
if (!allowed[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;
|
const schoolId = req.user.role === '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();
|
||||||
|
|
@ -286,7 +286,7 @@ export default async function api(app) {
|
||||||
|
|
||||||
// Aanpassen: naam, klas (uitwisselen), leerlingwachtwoord, nieuwe koppelcode, rol (super).
|
// Aanpassen: naam, klas (uitwisselen), leerlingwachtwoord, nieuwe koppelcode, rol (super).
|
||||||
app.patch('/admin/users/:id', async (req, reply) => {
|
app.patch('/admin/users/:id', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, PERMISSIONS['users.manage']);
|
||||||
const r = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
const r = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
||||||
const u = r.rows[0];
|
const u = r.rows[0];
|
||||||
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
||||||
|
|
@ -305,26 +305,26 @@ export default async function api(app) {
|
||||||
}
|
}
|
||||||
// groepsleiding/beheer: direct een nieuw wachtwoord zetten (zelf gekozen
|
// groepsleiding/beheer: direct een nieuw wachtwoord zetten (zelf gekozen
|
||||||
// of gegenereerd) zonder de koppelcode-activatie opnieuw te doorlopen.
|
// of gegenereerd) zonder de koppelcode-activatie opnieuw te doorlopen.
|
||||||
if ((b.password || b.generatePassword) && u.role !== 'pupil' && ['super', 'admin'].includes(req.user.role)) {
|
if ((b.password || b.generatePassword) && u.role !== 'pupil' && can(req.user.role, 'users.staffCredentials')) {
|
||||||
const pw = b.password || staffPassword();
|
const pw = b.password || staffPassword();
|
||||||
const err = checkPassword(pw, u.role);
|
const err = checkPassword(pw, u.role);
|
||||||
if (err) return fail(reply, 400, err);
|
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]);
|
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 };
|
return { ok: true, password: pw };
|
||||||
}
|
}
|
||||||
if (b.newLinkCode && u.role !== 'pupil' && ['super', 'admin'].includes(req.user.role)) {
|
if (b.newLinkCode && u.role !== 'pupil' && can(req.user.role, 'users.staffCredentials')) {
|
||||||
const code = newLinkCode();
|
const code = newLinkCode();
|
||||||
await pool.query('UPDATE users SET link_code = $1, password_hash = NULL WHERE id = $2', [code, u.id]);
|
await pool.query('UPDATE users SET link_code = $1, password_hash = NULL WHERE id = $2', [code, u.id]);
|
||||||
return { ok: true, linkCode: code };
|
return { ok: true, linkCode: code };
|
||||||
}
|
}
|
||||||
if (b.role && req.user.role === 'super' && ['super', 'admin', 'teacher'].includes(b.role) && u.role !== 'pupil')
|
if (b.role && can(req.user.role, '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',
|
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]);
|
[b.role, b.role === 'super' ? null : u.school_id, u.id]);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/admin/users/:id', async (req, reply) => {
|
app.delete('/admin/users/:id', async (req, reply) => {
|
||||||
need(req, reply, ['super', 'admin', 'teacher']);
|
need(req, reply, PERMISSIONS['users.manage']);
|
||||||
const r = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
const r = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
||||||
const u = r.rows[0];
|
const u = r.rows[0];
|
||||||
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
||||||
|
|
|
||||||
26
src/permissions.js
Normal file
26
src/permissions.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// teach: centrale permissiematrix voor de 4 vaste rollen (super/admin/teacher/pupil).
|
||||||
|
// Gespiegeld in public/js/permissions.js (geen bundler in dit project, dus twee
|
||||||
|
// bestanden die dezelfde waarden hand-in-hand bijhouden). Dit is geen database-
|
||||||
|
// gestuurd/rechten-per-gebruiker-configureerbaar systeem, maar één benoemde bron
|
||||||
|
// i.p.v. dezelfde rol-arrays los verspreid over de routes.
|
||||||
|
export const PERMISSIONS = {
|
||||||
|
'admin.access': ['super', 'admin', 'teacher'],
|
||||||
|
'password.own.change': ['super', 'admin', 'teacher'],
|
||||||
|
'schools.manage': ['super'],
|
||||||
|
'schools.resetPasswords': ['super', 'admin'],
|
||||||
|
'classes.view': ['super', 'admin', 'teacher'],
|
||||||
|
'classes.manage': ['super', 'admin'],
|
||||||
|
'users.manage': ['super', 'admin', 'teacher'],
|
||||||
|
'users.staffCredentials': ['super', 'admin'],
|
||||||
|
'users.genpw': ['super', 'admin', 'teacher'],
|
||||||
|
'users.role.change': ['super'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Welke rol een account met welke rol mag aanmaken.
|
||||||
|
export const CREATABLE_ROLES = {
|
||||||
|
super: ['super', 'admin', 'teacher', 'pupil'],
|
||||||
|
admin: ['admin', 'teacher', 'pupil'],
|
||||||
|
teacher: ['pupil'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const can = (role, action) => (PERMISSIONS[action] || []).includes(role);
|
||||||
Loading…
Reference in a new issue