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