feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan

- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
  encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
  status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
  disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
  (onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 12:23:57 +00:00
parent 0b3c23ff76
commit e55fd3baf0
16 changed files with 2402 additions and 152 deletions

View File

@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::fs;
use crate::totp::TotpData;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct OnboardingState {
complete: bool,
@@ -17,6 +19,8 @@ pub struct User {
pub password_hash: String,
pub setup_complete: bool,
pub onboarding_complete: bool,
#[serde(default)]
pub totp: Option<TotpData>,
}
#[allow(dead_code)]
@@ -58,6 +62,7 @@ impl AuthManager {
password_hash,
setup_complete: true,
onboarding_complete,
totp: None,
};
let user_file = self.data_dir.join("user.json");
@@ -123,6 +128,39 @@ impl AuthManager {
.unwrap_or(false))
}
/// Check if 2FA is enabled for the user.
pub async fn is_totp_enabled(&self) -> Result<bool> {
Ok(self.get_user().await?.map(|u| u.totp.is_some()).unwrap_or(false))
}
/// Get the TOTP data (if 2FA is enabled).
pub async fn get_totp_data(&self) -> Result<Option<TotpData>> {
Ok(self.get_user().await?.and_then(|u| u.totp))
}
/// Save TOTP data to user.json (enable 2FA).
pub async fn save_totp(&self, totp_data: TotpData) -> Result<()> {
let mut user = self.get_user().await?.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
user.totp = Some(totp_data);
let user_file = self.data_dir.join("user.json");
fs::write(&user_file, serde_json::to_string_pretty(&user)?).await?;
Ok(())
}
/// Remove TOTP data from user.json (disable 2FA).
pub async fn remove_totp(&self) -> Result<()> {
let mut user = self.get_user().await?.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
user.totp = None;
let user_file = self.data_dir.join("user.json");
fs::write(&user_file, serde_json::to_string_pretty(&user)?).await?;
Ok(())
}
/// Update TOTP data in place (e.g. after consuming a backup code or recording a used step).
pub async fn update_totp(&self, totp_data: TotpData) -> Result<()> {
self.save_totp(totp_data).await
}
pub async fn verify_password(&self, password: &str) -> Result<bool> {
use bcrypt::verify;
@@ -157,6 +195,13 @@ impl AuthManager {
.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
user.password_hash = password_hash;
// Re-encrypt TOTP MEK under new password if 2FA is enabled
if let Some(ref totp_data) = user.totp {
let rekeyed = crate::totp::rekey(totp_data, current_password, new_password)?;
user.totp = Some(rekeyed);
}
let user_file = self.data_dir.join("user.json");
let content = serde_json::to_string_pretty(&user)?;
fs::write(&user_file, content).await?;