use anyhow::Result; use chrono::Utc; use rusqlite::{Connection, params}; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PgUser { pub username: String, pub password_hash: String, pub email: Option, pub status: i32, pub home_dir: String, pub permissions: String, pub uid: i64, pub gid: i64, pub last_login: i64, pub created_at: i64, pub updated_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PgGroup { pub name: String, pub description: Option, pub created_at: i64, pub updated_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PgUserGroupMapping { pub username: String, pub group_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PgAdmin { pub username: String, pub password_hash: String, pub email: Option, pub description: Option, pub status: i32, pub permissions: String, pub filters: Option, pub role_id: Option, pub last_login: i64, pub created_at: i64, pub updated_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncResult { pub sync_type: String, pub sync_time: i64, pub users_synced: usize, pub users_failed: usize, pub groups_synced: usize, 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, } impl Default for SyncResult { fn default() -> Self { Self { sync_type: "unknown".to_string(), sync_time: Utc::now().timestamp(), users_synced: 0, users_failed: 0, groups_synced: 0, groups_failed: 0, mappings_synced: 0, mappings_failed: 0, admins_synced: 0, admins_failed: 0, status: "pending".to_string(), errors: Vec::new(), } } } impl SyncResult { pub fn success() -> Self { Self { status: "success".to_string(), ..Default::default() } } pub fn cached() -> Self { Self { status: "cached".to_string(), ..Default::default() } } pub fn failed(error: String) -> Self { Self { status: "failed".to_string(), errors: vec![error], ..Default::default() } } pub fn merge(&mut self, other: SyncResult) { self.users_synced += other.users_synced; self.users_failed += other.users_failed; self.groups_synced += other.groups_synced; self.groups_failed += other.groups_failed; self.mappings_synced += other.mappings_synced; self.mappings_failed += other.mappings_failed; self.errors.extend(other.errors); if self.users_failed > 0 || self.groups_failed > 0 || self.mappings_failed > 0 { if self.users_synced > 0 || self.groups_synced > 0 || self.mappings_synced > 0 { self.status = "partial_success".to_string(); } else { self.status = "failed".to_string(); } } } } #[derive(Clone)] pub struct AuthDb { pub path: String, } impl AuthDb { pub fn new(path: &str) -> Result { if !Path::new(path).exists() { Self::init_db(path)?; } Ok(Self { path: path.to_string() }) } pub fn init_db(path: &str) -> Result<()> { let conn = Connection::open(path)?; conn.execute_batch(include_str!("../data/init_auth_db.sql"))?; Ok(()) } pub fn open(&self) -> Result { Ok(Connection::open(&self.path)?) } pub fn save_user(&self, user: &PgUser) -> Result<()> { let conn = self.open()?; let now = Utc::now().timestamp(); conn.execute( "INSERT OR REPLACE INTO sftpgo_users (username, password_hash, email, status, home_dir, permissions, uid, gid, last_login, created_at, updated_at, last_sync_at, sync_status) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", params![ user.username, user.password_hash, user.email, user.status, user.home_dir, user.permissions, user.uid, user.gid, user.last_login, user.created_at, user.updated_at, now, 1, // sync_status = synced ] )?; Ok(()) } pub fn save_group(&self, group: &PgGroup) -> Result<()> { let conn = self.open()?; let now = Utc::now().timestamp(); conn.execute( "INSERT OR REPLACE INTO sftpgo_groups (name, description, created_at, updated_at, last_sync_at) VALUES (?1, ?2, ?3, ?4, ?5)", params![ group.name, group.description, group.created_at, group.updated_at, now, ] )?; Ok(()) } pub fn save_mapping(&self, mapping: &PgUserGroupMapping) -> Result<()> { let conn = self.open()?; let now = Utc::now().timestamp(); conn.execute( "INSERT OR REPLACE INTO users_groups_mapping (username, group_name, created_at) VALUES (?1, ?2, ?3)", params![ mapping.username, mapping.group_name, now, ] )?; Ok(()) } pub fn save_sync_log(&self, result: &SyncResult) -> Result<()> { let conn = self.open()?; conn.execute( "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, ?10, ?11, ?12)", params![ result.sync_type, result.sync_time, result.users_synced, result.users_failed, 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={}, 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> { 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) -> Result { 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> { let conn = self.open()?; let result = conn.query_row( "SELECT username, password_hash, email, status, home_dir, permissions, uid, gid, last_login, created_at, updated_at FROM sftpgo_users WHERE username = ?1 AND status = 1", params![username], |row| Ok(PgUser { username: row.get(0)?, password_hash: row.get(1)?, email: row.get(2)?, status: row.get(3)?, home_dir: row.get(4)?, permissions: row.get(5)?, uid: row.get(6)?, gid: row.get(7)?, last_login: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, }) ); match result { Ok(user) => Ok(Some(user)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(e.into()), } } pub fn get_user_groups(&self, username: &str) -> Result> { let conn = self.open()?; let groups: Vec = conn .prepare( "SELECT group_name FROM users_groups_mapping WHERE username = ?1" )? .query_map(params![username], |row| row.get(0))? .collect::, _>>()?; Ok(groups) } }