mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 18:21:45 +08:00
feat: 添加 TOTP 2FA 功能
This commit is contained in:
@@ -7,8 +7,6 @@ use typeshare::typeshare;
|
||||
pub struct AuthConfig {
|
||||
pub session_timeout_secs: u32,
|
||||
pub single_user_allow_multiple_sessions: bool,
|
||||
pub totp_enabled: bool,
|
||||
pub totp_secret: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AuthConfig {
|
||||
@@ -16,8 +14,6 @@ impl Default for AuthConfig {
|
||||
Self {
|
||||
session_timeout_secs: 3600 * 24,
|
||||
single_user_allow_multiple_sessions: false,
|
||||
totp_enabled: false,
|
||||
totp_secret: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,16 @@ impl ConfigStore {
|
||||
}
|
||||
|
||||
pub async fn load(&self) -> Result<()> {
|
||||
let mut config = Self::load_config(&self.pool).await?;
|
||||
let (mut config, removed_legacy_totp) = Self::load_config(&self.pool).await?;
|
||||
config.enforce_invariants();
|
||||
if removed_legacy_totp {
|
||||
Self::save_config_to_db(&self.pool, &config).await?;
|
||||
}
|
||||
self.cache.store(Arc::new(config));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_config(pool: &Pool<Sqlite>) -> Result<AppConfig> {
|
||||
async fn load_config(pool: &Pool<Sqlite>) -> Result<(AppConfig, bool)> {
|
||||
let row: Option<(String,)> =
|
||||
sqlx::query_as("SELECT value FROM config WHERE key = 'app_config'")
|
||||
.fetch_optional(pool)
|
||||
@@ -41,12 +44,24 @@ impl ConfigStore {
|
||||
|
||||
match row {
|
||||
Some((json,)) => {
|
||||
serde_json::from_str(&json).map_err(|e| AppError::Config(e.to_string()))
|
||||
let mut value: serde_json::Value =
|
||||
serde_json::from_str(&json).map_err(|e| AppError::Config(e.to_string()))?;
|
||||
let mut removed = false;
|
||||
if let Some(auth) = value
|
||||
.get_mut("auth")
|
||||
.and_then(|value| value.as_object_mut())
|
||||
{
|
||||
removed |= auth.remove("totp_enabled").is_some();
|
||||
removed |= auth.remove("totp_secret").is_some();
|
||||
}
|
||||
let config =
|
||||
serde_json::from_value(value).map_err(|e| AppError::Config(e.to_string()))?;
|
||||
Ok((config, removed))
|
||||
}
|
||||
None => {
|
||||
let config = AppConfig::default();
|
||||
Self::save_config_to_db(pool, &config).await?;
|
||||
Ok(config)
|
||||
Ok((config, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,4 +189,35 @@ mod tests {
|
||||
.is_err());
|
||||
assert!(!store.get().watchdog.enabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_removes_legacy_totp_fields_from_persisted_config() {
|
||||
let dir = tempdir().unwrap();
|
||||
let db = DatabasePool::new(&dir.path().join("test.db"))
|
||||
.await
|
||||
.unwrap();
|
||||
db.init_schema().await.unwrap();
|
||||
let mut value = serde_json::to_value(AppConfig::default()).unwrap();
|
||||
let auth = value.get_mut("auth").unwrap().as_object_mut().unwrap();
|
||||
auth.insert("totp_enabled".to_string(), serde_json::json!(true));
|
||||
auth.insert(
|
||||
"totp_secret".to_string(),
|
||||
serde_json::json!("legacy-secret"),
|
||||
);
|
||||
sqlx::query("INSERT INTO config (key, value) VALUES ('app_config', ?1)")
|
||||
.bind(value.to_string())
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let store = ConfigStore::new(db.clone_pool()).unwrap();
|
||||
store.load().await.unwrap();
|
||||
let (persisted,): (String,) =
|
||||
sqlx::query_as("SELECT value FROM config WHERE key = 'app_config'")
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!persisted.contains("totp_enabled"));
|
||||
assert!(!persisted.contains("totp_secret"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user