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

@@ -39,11 +39,33 @@ pub struct LoginResponse {
pub permissions: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminLoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminSession {
pub token: String,
pub username: String,
pub created_at: String,
pub expires_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminLoginResponse {
pub token: String,
pub expires_at: String,
pub username: String,
}
#[derive(Clone)]
pub struct AuthState {
pub sessions: Arc<Mutex<HashMap<String, Session>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
pub auth_db: Option<crate::sync::AuthDb>,
pub admin_sessions: Arc<Mutex<HashMap<String, AdminSession>>>,
}
impl AuthState {
@@ -66,6 +88,7 @@ impl AuthState {
sessions: Arc::new(Mutex::new(HashMap::new())),
users: Arc::new(Mutex::new(users)),
auth_db: None,
admin_sessions: Arc::new(Mutex::new(HashMap::new())),
}
}
@@ -76,6 +99,7 @@ impl AuthState {
sessions: Arc::new(Mutex::new(HashMap::new())),
users: Arc::new(Mutex::new(HashMap::new())),
auth_db,
admin_sessions: Arc::new(Mutex::new(HashMap::new())),
}
}
@@ -109,11 +133,81 @@ impl AuthState {
permissions: "{}".to_string(),
})
} else {
None
}
}
pub fn admin_login(&self, username: &str, password: &str) -> Option<AdminLoginResponse> {
if let Some(auth_db) = &self.auth_db {
match auth_db.get_admin(username) {
Ok(Some(admin)) if admin.status == 1 => {
if verify(password, &admin.password_hash).unwrap_or(false) {
let token = Uuid::new_v4().to_string();
let now = Utc::now();
let expires_at = now + Duration::hours(24);
let session = AdminSession {
token: token.clone(),
username: username.to_string(),
created_at: now.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
};
let mut admin_sessions = self.admin_sessions.lock().unwrap();
admin_sessions.insert(token.clone(), session);
log::info!("Admin {} logged in successfully", username);
Some(AdminLoginResponse {
token,
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
username: username.to_string(),
})
} else {
log::warn!("Invalid password for admin {}", username);
None
}
}
Ok(Some(_)) => {
log::warn!("Admin {} is not active", username);
None
}
Ok(None) => {
log::warn!("Admin {} not found", username);
None
}
Err(e) => {
log::error!("Failed to get admin {}: {}", username, e);
None
}
}
} else {
log::warn!("Auth DB not available for admin login");
None
}
}
pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginResponse> {
pub fn verify_admin_token(&self, token: &str) -> Option<AdminSession> {
let admin_sessions = self.admin_sessions.lock().unwrap();
if let Some(session) = admin_sessions.get(token) {
let expires_at = chrono::DateTime::parse_from_rfc3339(&session.expires_at)
.ok()
.map(|dt| dt.with_timezone(&Utc));
if let Some(exp) = expires_at {
if Utc::now() < exp {
return Some(session.clone());
} else {
log::warn!("Admin token {} has expired", token);
}
}
}
None
}
pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginResponse> {
if let Some(auth_db) = &self.auth_db {
// Get user from auth.sqlite
let user = match auth_db.get_user(username) {
@@ -135,7 +229,7 @@ pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginRes
if verify(password, &user.password_hash).unwrap_or(false) {
let groups = auth_db.get_user_groups(username).unwrap_or_default();
let permissions = user.permissions.clone();
let permissions = user.permissions.clone();
let token = Uuid::new_v4().to_string();
let now = Utc::now();