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:
98
src/auth.rs
98
src/auth.rs
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user