Dorian 99400a7165 feat: container orchestration, branding overhaul, onboarding logging
Container orchestration:
- Health monitor with crash recovery and auto-restart
- Doctor service (periodic health checks via systemd timer)
- Reconcile service (desired-state convergence)
- Stack-aware install/uninstall with dependency tracking

Branding:
- Custom GRUB background (designer artwork, 1024x768)
- ISOLINUX boot menu: centered, orange accents, clean labels
- Terminal banners: adaptive width, basic ANSI colors, fits 80-col
- Removed auto-generated splash scripts (designer provides assets)
- GRUB theme: lowercase branding

Frontend:
- 401 handler clears localStorage immediately (prevents cascade)

Backend:
- Onboarding/auth logging ([onboarding] tag in journalctl)
- Cookie Secure flag logging for debugging HTTP/HTTPS issues

ISO fixes:
- Install log saved before unmount (was silently failing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:34:29 +00:00

139 lines
5.3 KiB
Rust

use super::{RpcHandler, DEV_DEFAULT_PASSWORD};
use anyhow::Result;
impl RpcHandler {
pub(super) async fn handle_auth_login(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let password = params
.get("password")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
let is_setup = self.auth_manager.is_setup().await?;
if !is_setup {
// Dev mode: allow default password so UI can log in without running setup
if self.config.dev_mode && password == DEV_DEFAULT_PASSWORD {
tracing::info!("[onboarding] login via dev default password");
return Ok(serde_json::Value::Null);
}
tracing::warn!("[onboarding] login attempt before setup complete");
return Err(anyhow::anyhow!(
"User not set up. Please complete setup first."
));
}
let valid = self.auth_manager.verify_password(password).await?;
if !valid {
tracing::warn!("[onboarding] login failed — wrong password");
return Err(anyhow::anyhow!("Password Incorrect"));
}
tracing::info!("[onboarding] login successful");
Ok(serde_json::Value::Null)
}
pub(super) async fn handle_auth_logout(&self) -> Result<serde_json::Value> {
tracing::info!("[onboarding] logout");
Ok(serde_json::Value::Null)
}
pub(super) async fn handle_auth_change_password(
&self,
params: Option<serde_json::Value>,
session_token: &Option<String>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let current_password = params
.get("currentPassword")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing currentPassword"))?;
let new_password = params
.get("newPassword")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing newPassword"))?;
let also_change_ssh = params
.get("alsoChangeSsh")
.and_then(|v| v.as_bool())
.unwrap_or(true);
self.auth_manager
.change_password(current_password, new_password, also_change_ssh)
.await?;
// Session rotation: invalidate all other sessions, rotate the caller's session
if let Some(token) = session_token {
self.session_store.invalidate_all_except(token).await;
}
Ok(serde_json::json!({ "success": true, "session_rotated": true }))
}
pub(super) async fn handle_auth_is_setup(&self) -> Result<serde_json::Value> {
let is_setup = self.auth_manager.is_setup().await?;
Ok(serde_json::json!(is_setup))
}
pub(super) async fn handle_auth_setup(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
// Prevent re-setup if already set up
let is_setup = self.auth_manager.is_setup().await?;
if is_setup {
tracing::warn!("[onboarding] setup rejected — already set up");
return Err(anyhow::anyhow!("Already set up. Use auth.changePassword to change."));
}
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let password = params
.get("password")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
if password.len() < 8 {
tracing::warn!("[onboarding] setup rejected — password too short");
return Err(anyhow::anyhow!("Password must be at least 8 characters"));
}
self.auth_manager.setup_user(password).await?;
tracing::info!("[onboarding] user setup complete");
Ok(serde_json::json!(true))
}
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
self.auth_manager.complete_onboarding().await?;
tracing::info!("[onboarding] onboarding marked complete");
Ok(serde_json::json!(true))
}
pub(super) async fn handle_auth_is_onboarding_complete(&self) -> Result<serde_json::Value> {
let complete = self.auth_manager.is_onboarding_complete().await?;
tracing::debug!("[onboarding] isOnboardingComplete={}", complete);
Ok(serde_json::json!(complete))
}
pub(super) async fn handle_auth_reset_onboarding(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let password = params
.get("password")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing password — re-authentication required"))?;
let valid = self.auth_manager.verify_password(password).await?;
if !valid {
tracing::warn!("[onboarding] reset rejected — wrong password");
return Err(anyhow::anyhow!("Password Incorrect"));
}
self.auth_manager.reset_onboarding().await?;
tracing::info!("[onboarding] onboarding reset");
Ok(serde_json::json!(true))
}
}