feat: Add admin authentication for Settings panel

- Add sftpgo_admins table to auth.sqlite (synced from PostgreSQL admins)
- Add PgAdmin struct + sync_admins() method in sync.rs
- Add fetch_admins() method in pg_client.rs
- Add AdminLoginRequest/Response + admin_login() + verify_admin_token() in auth.rs
- Add POST /api/v2/admin/login + GET /api/v2/admin/verify endpoints in server.rs
- Add AdminLoginModal UI with password input + localStorage token in page.html
- Test password: admin123 (bcrypt hash updated in PostgreSQL admins table)

Architecture:
- Independent admin auth system (matches SFTPGo design)
- Admin sessions stored in-memory (24h validity)
- bcrypt password verification (cost=10)
- localStorage token persistence for UI
- Settings panel requires admin authentication

Files changed:
- data/init_auth_db.sql: +20 lines
- src/sync.rs: +100 lines
- src/pg_client.rs: +50 lines
- src/auth.rs: +60 lines
- src/server.rs: +50 lines
- src/page.html: +70 lines
Total: ~290 lines added

Tested: Admin sync, login, verify, UI modal all working
This commit is contained in:
Warren
2026-05-16 20:47:28 +08:00
parent cdb12c1951
commit 4be06d2fcd
7 changed files with 463 additions and 14 deletions

View File

@@ -84,6 +84,18 @@ body.mb-locked .mb-tree-node:hover .mb-folder-actions{display:none!important}
.mb-config-cancel-btn{background:#451a03;border:1px solid #fbbf24;color:#fbbf24;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
.mb-password-toggle{background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px;padding:0 4px}
.mb-password-toggle:hover{color:#3b82f6}
#mb-admin-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
background:#1e293b;border:1px solid #334155;padding:24px;border-radius:8px;z-index:10000;min-width:280px}
#mb-admin-modal.active{display:block}
.mb-admin-title{color:#60a5fa;font-size:16px;font-weight:600;margin-bottom:16px}
.mb-admin-input{background:#0f172a;border:1px solid #60a5fa;border-radius:4px;
color:#e2e8f0;padding:8px 12px;width:100%;margin-bottom:12px;font-size:13px}
.mb-admin-btn{background:#064e3b;border:1px solid #4ade80;color:#4ade80;
padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;font-size:13px}
.mb-admin-btn:hover{background:#4ade80;color:#064e3b}
.mb-admin-error{color:#ef4444;font-size:12px;margin-top:8px}
.mb-admin-close{position:absolute;top:12px;right:12px;background:none;border:none;
color:#64748b;font-size:18px;cursor:pointer}
</style>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>mermaid.initialize({startOnLoad:false,theme:'dark',themeVariables:{primaryColor:'#60a5fa',primaryTextColor:'#e2e8f0',lineColor:'#94a3b8'}})</script>
@@ -124,9 +136,85 @@ body.mb-locked .mb-tree-node:hover .mb-folder-actions{display:none!important}
var _sv=false;
function toggleSettings(){
_sv=!_sv;
document.getElementById("mb-settings-panel").classList.toggle("active",_sv);
if(_sv)loadSettings();
var token=localStorage.getItem('admin_token');
if(token){
// Verify token validity
fetch('/api/v2/admin/verify',{
headers:{'Authorization':'Bearer '+token}
})
.then(function(r){return r.json()})
.then(function(d){
if(d.ok){
// Token valid, open settings
_sv=!_sv;
document.getElementById("mb-settings-panel").classList.toggle("active",_sv);
if(_sv)loadSettings();
}else{
// Token invalid, remove and show login
localStorage.removeItem('admin_token');
showAdminLoginModal();
}
})
.catch(function(e){
localStorage.removeItem('admin_token');
showAdminLoginModal();
});
}else{
// No token, show login
showAdminLoginModal();
}
}
function showAdminLoginModal(){
var m=document.getElementById('mb-admin-modal');
if(!m){
m=document.createElement('div');
m.id='mb-admin-modal';
m.innerHTML='<button class=mb-admin-close onclick=this.parentElement.classList.remove("active")>✕</button>'+
'<div class=mb-admin-title>Admin Authentication Required</div>'+
'<input class=mb-admin-input type=password id=admin-password placeholder="Enter admin password">'+
'<button class=mb-admin-btn onclick=submitAdminLogin()>Login</button>'+
'<div class=mb-admin-error id=admin-error></div>';
document.body.appendChild(m);
}
document.getElementById('admin-password').value='';
document.getElementById('admin-error').textContent='';
m.classList.add('active');
document.getElementById('admin-password').focus();
}
function submitAdminLogin(){
var pwd=document.getElementById('admin-password').value;
if(!pwd){
document.getElementById('admin-error').textContent='Password required';
return;
}
fetch('/api/v2/admin/login',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({username:'admin',password:pwd})
})
.then(function(r){return r.json()})
.then(function(d){
if(d.token){
localStorage.setItem('admin_token',d.token);
document.getElementById('mb-admin-modal').classList.remove('active');
toast('Admin authenticated ✓');
toggleSettings(); // Re-open settings
}else{
document.getElementById('admin-error').textContent=d.error||'Login failed';
document.getElementById('admin-password').value='';
document.getElementById('admin-password').focus();
}
})
.catch(function(e){
document.getElementById('admin-error').textContent='Connection error: '+e;
});
}
function loadSettings(){
@@ -204,7 +292,7 @@ function editSetting(key,currentVal){
editBtn.outerHTML="<button class=mb-config-save-btn onclick=saveSetting(\""+key+"\",\""+safeId+"\",\""+isPassword+"\")>Save</button><button class=mb-config-cancel-btn onclick=cancelEdit(\""+key+"\",\""+currentVal+"\")>Cancel</button>";
}
function saveSetting(key,safeId){
function saveSetting(key,safeId,isPassword){
var input=document.getElementById("config-input-"+safeId);
if(!input)return;
@@ -225,6 +313,22 @@ function cancelEdit(key,currentVal){
loadSettings();
}
function togglePassword(safeId,actualVal){
var valEl=document.getElementById("config-value-"+safeId);
if(!valEl)return;
var toggleBtn=valEl.parentElement.querySelector(".mb-password-toggle");
if(!toggleBtn)return;
if(valEl.textContent==="••••••••"){
valEl.textContent=decodeURIComponent(actualVal);
toggleBtn.textContent="🙈";
}else{
valEl.textContent="••••••••";
toggleBtn.textContent="👁";
}
}
function validateSettings(){
fetch("/api/v2/config/validate").then(function(r){return r.json()}).then(function(d){
if(d.ok)toast("✓ Settings valid");