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

@@ -33,6 +33,21 @@ pub struct PgUserGroupMapping {
pub group_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PgAdmin {
pub username: String,
pub password_hash: String,
pub email: Option<String>,
pub description: Option<String>,
pub status: i32,
pub permissions: String,
pub filters: Option<String>,
pub role_id: Option<i32>,
pub last_login: i64,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResult {
pub sync_type: String,
@@ -43,6 +58,8 @@ pub struct SyncResult {
pub groups_failed: usize,
pub mappings_synced: usize,
pub mappings_failed: usize,
pub admins_synced: usize,
pub admins_failed: usize,
pub status: String,
pub errors: Vec<String>,
}
@@ -58,6 +75,8 @@ impl Default for SyncResult {
groups_failed: 0,
mappings_synced: 0,
mappings_failed: 0,
admins_synced: 0,
admins_failed: 0,
status: "pending".to_string(),
errors: Vec::new(),
}
@@ -203,8 +222,9 @@ impl AuthDb {
"INSERT INTO sync_log
(sync_type, sync_time, users_synced, users_failed,
groups_synced, groups_failed, mappings_synced,
mappings_failed, admins_synced, admins_failed,
status, error_message)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
result.sync_type,
result.sync_time,
@@ -213,22 +233,98 @@ impl AuthDb {
result.groups_synced,
result.groups_failed,
result.mappings_synced,
result.mappings_failed,
result.admins_synced,
result.admins_failed,
result.status,
result.errors.join(";"),
]
)?;
log::info!(
"Sync log saved: users={}, groups={}, mappings={}, status={}",
"Sync log saved: users={}, groups={}, mappings={}, admins={}, status={}",
result.users_synced,
result.groups_synced,
result.mappings_synced,
result.admins_synced,
result.status
);
Ok(())
}
pub fn save_admin(&self, admin: &PgAdmin) -> Result<()> {
let conn = self.open()?;
conn.execute(
"INSERT OR REPLACE INTO sftpgo_admins
(username, password_hash, email, description, status,
permissions, filters, role_id, last_login,
created_at, updated_at, last_sync_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
admin.username,
admin.password_hash,
admin.email,
admin.description,
admin.status,
admin.permissions,
admin.filters,
admin.role_id,
admin.last_login,
admin.created_at,
admin.updated_at,
Utc::now().timestamp(),
],
)?;
Ok(())
}
pub fn get_admin(&self, username: &str) -> Result<Option<PgAdmin>> {
let conn = self.open()?;
let result = conn.query_row(
"SELECT username, password_hash, email, description, status,
permissions, filters, role_id, last_login,
created_at, updated_at
FROM sftpgo_admins WHERE username = ?1 AND status = 1",
params![username],
|row| Ok(PgAdmin {
username: row.get(0)?,
password_hash: row.get(1)?,
email: row.get(2)?,
description: row.get(3)?,
status: row.get(4)?,
permissions: row.get(5)?,
filters: row.get(6)?,
role_id: row.get(7)?,
last_login: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
}),
);
match result {
Ok(admin) => Ok(Some(admin)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn sync_admins(&self, admins: Vec<PgAdmin>) -> Result<usize> {
let mut synced = 0;
for admin in admins {
match self.save_admin(&admin) {
Ok(_) => synced += 1,
Err(e) => log::warn!("Failed to sync admin {}: {}", admin.username, e),
}
}
Ok(synced)
}
pub fn get_user(&self, username: &str) -> Result<Option<PgUser>> {
let conn = self.open()?;