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

@@ -1,6 +1,6 @@
use anyhow::Result;
use tokio_postgres::{NoTls, Client};
use crate::sync::{PgUser, PgGroup, PgUserGroupMapping};
use crate::sync::{PgUser, PgGroup, PgUserGroupMapping, PgAdmin, SyncResult};
pub struct PgClient {
host: String,
@@ -105,6 +105,36 @@ impl PgClient {
Ok(groups)
}
pub async fn fetch_admins(&self) -> Result<Vec<PgAdmin>> {
let rows = sqlx::query!(
"SELECT username, password as password_hash,
email, description, status, permissions, filters,
role_id, last_login, created_at, updated_at
FROM admins WHERE status = 1"
)
.fetch_all(&self.pool)
.await?;
let admins = rows
.into_iter()
.map(|row| PgAdmin {
username: row.username,
password_hash: row.password_hash,
email: row.email,
description: row.description,
status: row.status,
permissions: row.permissions,
filters: row.filters,
role_id: row.role_id.map(|v| v as i32),
last_login: row.last_login,
created_at: row.created_at,
updated_at: row.updated_at,
})
.collect();
Ok(admins)
}
pub async fn fetch_mappings(&self) -> Result<Vec<PgUserGroupMapping>> {
let client = self.connect().await?;
@@ -127,6 +157,37 @@ impl PgClient {
Ok(mappings)
}
pub async fn fetch_admins(&self, client: &Client) -> Result<Vec<PgAdmin>> {
let rows = client
.query(
"SELECT username, password, email, description, status,
permissions, filters, role_id, last_login,
created_at, updated_at
FROM admins WHERE status = 1",
&[],
)
.await?;
let admins = rows
.into_iter()
.map(|row| PgAdmin {
username: row.get::<_, String>(0),
password_hash: row.get::<_, String>(1),
email: row.get::<_, Option<String>>(2),
description: row.get::<_, Option<String>>(3),
status: row.get::<_, i32>(4),
permissions: row.get::<_, String>(5),
filters: row.get::<_, Option<String>>(6),
role_id: row.get::<_, Option<i32>>(7),
last_login: row.get::<_, i64>(8),
created_at: row.get::<_, i64>(9),
updated_at: row.get::<_, i64>(10),
})
.collect();
Ok(admins)
}
}
pub struct SftpGoSync {
@@ -221,9 +282,40 @@ impl SftpGoSync {
}
}
// 4. Determine final status
if result.users_failed > 0 || result.groups_failed > 0 || result.mappings_failed > 0 {
if result.users_synced > 0 || result.groups_synced > 0 || result.mappings_synced > 0 {
// 4. Sync admins
match self.pg_client.connect().await {
Ok(client) => {
match self.pg_client.fetch_admins(&client).await {
Ok(admins) => {
log::info!("Fetched {} admins from PostgreSQL", admins.len());
for admin in admins {
match self.auth_db.sync_admins(vec![admin.clone()]) {
Ok(_) => result.admins_synced += 1,
Err(e) => {
result.admins_failed += 1;
result.errors.push(format!("Admin {} sync failed: {}", admin.username, e));
log::error!("Failed to sync admin {}: {}", admin.username, e);
}
}
}
}
Err(e) => {
log::error!("Failed to fetch admins from PostgreSQL: {}", e);
result.errors.push(format!("PG admins fetch failed: {}", e));
result.admins_failed = 1;
}
}
}
Err(e) => {
log::error!("Failed to connect to PostgreSQL for admins sync: {}", e);
result.errors.push(format!("PG connection failed for admins: {}", e));
result.admins_failed = 1;
}
}
// 5. Determine final status
if result.users_failed > 0 || result.groups_failed > 0 || result.mappings_failed > 0 || result.admins_failed > 0 {
if result.users_synced > 0 || result.groups_synced > 0 || result.mappings_synced > 0 || result.admins_synced > 0 {
result.status = "partial_success".to_string();
} else {
result.status = "cached".to_string();
@@ -232,14 +324,15 @@ impl SftpGoSync {
result.status = "success".to_string();
}
// 5. Save sync log
// 6. Save sync log
self.auth_db.save_sync_log(&result)?;
log::info!(
"Sync completed: users={}, groups={}, mappings={}, status={}",
"Sync completed: users={}, groups={}, mappings={}, admins={}, status={}",
result.users_synced,
result.groups_synced,
result.mappings_synced,
result.admins_synced,
result.status
);