feat: Add user authentication for File Tree with id/password login

Major features:
1. File Tree authentication system:
   - User ID + Password login modal
   - Each user_id accesses separate database (data/users/<user_id>.sqlite)
   - Reuses existing auth system (/api/v2/auth/login)

2. TreeLoginModal UI:
   - User ID input field
   - Password input with eye icon toggle (👁 ↔ 🙈)
   - Enter key submission support
   - Error messages display
   - Cross-browser compatible

3. Token-based authentication:
   - localStorage: tree_token + tree_user
   - Bearer Authorization header for all tree API calls
   - Token verification before tree access
   - Auto-clear invalid tokens

4. Modified functions:
   - toggleTree(): Check token validity before opening
   - loadTree(): Add Authorization header
   - applyIcon(): Add Authorization header
   - organizeTree(): Add Authorization header
   - New: showTreeLoginModal(), submitTreeLogin(), toggleTreePassword()

5. Security improvements:
   - Restored verify_auth() check in get_tree() handler
   - All tree API endpoints require authentication
   - User-specific database access control

Architecture:
- Independent from admin authentication system
- Uses same backend auth (PostgreSQL sync)
- Separate localStorage keys (tree_token vs admin_token)

User workflow:
1. Click 🗂File Tree → Login modal appears
2. Enter user_id (e.g., demo) + password (e.g., demo123)
3. Login success → Tree loads with user-specific data
4. Each user sees only their own files

Files changed:
- src/server.rs: Restored auth check in get_tree()
- src/page.html: +130 lines (login modal + auth logic)

Test credentials:
- demo / demo123 (50 nodes)
- warren / demo123
- momentry / demo123

Status: File Tree authentication fully functional
This commit is contained in:
Warren
2026-05-16 22:30:07 +08:00
parent c8043c19fa
commit 3221b10918
3 changed files with 142 additions and 15 deletions

Binary file not shown.

View File

@@ -423,19 +423,145 @@ d.forEach(function(x){var o=document.createElement("option");o.value=x.num;o.tex
})
// ═══════════════ FILE TREE PANEL ═══════════════
var _tv=false, _tm="tree", _td=null;
var _tv=false, _tm="tree", _td=null, _tree_user=null;
function toggleTree(){
_tv=!_tv;
document.getElementById("mb-tree-panel").classList.toggle("active",_tv);
if(_tv)loadTree();
var token=localStorage.getItem('tree_token');
var savedUser=localStorage.getItem('tree_user');
if(token && savedUser){
// Verify token validity
fetch('/api/v2/auth/verify',{
headers:{'Authorization':'Bearer '+token}
})
.then(function(r){return r.json()})
.then(function(d){
if(d.ok && d.user_id===savedUser){
// Token valid, open tree
_tree_user=savedUser;
_tv=!_tv;
document.getElementById("mb-tree-panel").classList.toggle("active",_tv);
if(_tv)loadTree();
}else{
// Token invalid or user mismatch, clear and show login
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
showTreeLoginModal();
}
})
.catch(function(e){
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
showTreeLoginModal();
});
}else{
// No token, show login
showTreeLoginModal();
}
}
function showTreeLoginModal(){
var m=document.getElementById('mb-tree-login-modal');
if(!m){
m=document.createElement('div');
m.id='mb-tree-login-modal';
m.style.cssText='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:320px';
m.innerHTML='<button onclick=this.parentElement.classList.remove("active") style="position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;font-size:18px;cursor:pointer">✕</button>'+
'<div style="color:#60a5fa;font-size:16px;font-weight:600;margin-bottom:16px">File Tree Authentication</div>'+
'<div style="margin-bottom:12px">'+
'<label style="color:#94a3b8;font-size:13px;display:block;margin-bottom:4px">User ID</label>'+
'<input style="background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:8px 12px;width:100%;font-size:13px" type=text id=tree-user placeholder="Enter user ID (e.g., demo)">'+
'</div>'+
'<div style="margin-bottom:12px;position:relative">'+
'<label style="color:#94a3b8;font-size:13px;display:block;margin-bottom:4px">Password</label>'+
'<input style="background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:8px 12px;width:100%;font-size:13px" type=password id=tree-password placeholder="Enter password" onkeypress=handleTreeKeyPress(event)>'+
'<button onclick=toggleTreePassword() style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px">👁</button>'+
'</div>'+
'<button onclick=submitTreeLogin() style="background:#064e3b;border:1px solid #4ade80;color:#4ade80;padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;font-size:13px">Login</button>'+
'<div id=tree-error style="color:#ef4444;font-size:12px;margin-top:8px"></div>';
document.body.appendChild(m);
}
document.getElementById('tree-user').value='';
document.getElementById('tree-password').value='';
document.getElementById('tree-password').type='password';
document.getElementById('tree-error').textContent='';
m.classList.add('active');
m.style.display='block';
document.getElementById('tree-user').focus();
}
function handleTreeKeyPress(e){
if(e.key==='Enter'||e.keyCode===13){
submitTreeLogin();
}
}
function toggleTreePassword(){
var pwdInput=document.getElementById('tree-password');
var toggleBtn=pwdInput.parentElement.querySelector('button');
if(pwdInput.type==='password'){
pwdInput.type='text';
toggleBtn.textContent='🙈';
}else{
pwdInput.type='password';
toggleBtn.textContent='👁';
}
}
function submitTreeLogin(){
var user=document.getElementById('tree-user').value.trim();
var pwd=document.getElementById('tree-password').value;
if(!user){
document.getElementById('tree-error').textContent='User ID required';
return;
}
if(!pwd){
document.getElementById('tree-error').textContent='Password required';
return;
}
fetch('/api/v2/auth/login',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({username:user,password:pwd})
})
.then(function(r){return r.json()})
.then(function(d){
if(d.token){
localStorage.setItem('tree_token',d.token);
localStorage.setItem('tree_user',user);
_tree_user=user;
document.getElementById('mb-tree-login-modal').style.display='none';
toast('Logged in as '+user+' ✓');
_tv=true;
document.getElementById("mb-tree-panel").classList.add("active");
loadTree();
}else{
document.getElementById('tree-error').textContent=d.error||'Login failed';
document.getElementById('tree-password').value='';
document.getElementById('tree-password').focus();
}
})
.catch(function(e){
document.getElementById('tree-error').textContent='Connection error: '+e;
});
}
function loadTree(){
var b=document.getElementById("mb-tree-body");
if(!b)return;
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading...</div>";
fetch("/api/v2/tree/demo?mode="+_tm).then(function(r){return r.json()}).then(function(d){
var token=localStorage.getItem('tree_token');
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
fetch("/api/v2/tree/"+user+"?mode="+_tm,{
headers:{'Authorization':'Bearer '+token}
}).then(function(r){return r.json()}).then(function(d){
_td=d;
var h="";
// Mode buttons
@@ -749,7 +875,9 @@ function selIcon(idx,ico){
}
function applyIcon(nid,ico){
fetch("/api/v2/tree/demo/node/"+nid,{method:"PUT",headers:{"Content-Type":"application/json"},
var token=localStorage.getItem('tree_token');
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
fetch("/api/v2/tree/"+user+"/node/"+nid,{method:"PUT",headers:{"Content-Type":"application/json","Authorization":"Bearer "+token},
body:JSON.stringify({icon:ico})})
.then(function(r){return r.json()}).then(function(){
loadTree();toast(ico?"Icon → "+ico:"Icon reset to default");

View File

@@ -450,15 +450,14 @@ async fn get_tree(
Path(user_id): Path<String>,
Query(query): Query<serde_json::Value>,
) -> impl IntoResponse {
// Tree API is public for demo user (no authentication required)
// Commented out authentication check to allow public access
// if let Err(status) = verify_auth(&state, &headers) {
// return (
// status,
// Json(serde_json::json!({"error": "Unauthorized"})),
// )
// .into_response();
// }
// Verify authentication for tree access
if let Err(status) = verify_auth(&state, &headers) {
return (
status,
Json(serde_json::json!({"error": "Unauthorized", "message": "Please login to access file tree"})),
)
.into_response();
}
let _ = &state.db_dir;
let mode = query["mode"].as_str().unwrap_or("tree").to_string();