Update Fedimint configuration and enhance onboarding process
- Upgraded Fedimint version to v0.10.0 in docker-compose.yml and manifest.yml, adding support for the built-in Guardian UI. - Modified .gitignore to exclude deploy-config.sh script. - Enhanced onboarding process in AuthManager to persist onboarding state and validate password strength during user setup. - Updated API to handle onboarding completion and password change requests, ensuring a smoother user experience. - Improved configuration management to support Nostr discovery and Tor proxy settings, enhancing node identity features.
This commit is contained in:
@@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct OnboardingState {
|
||||
complete: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
@@ -43,13 +48,16 @@ impl AuthManager {
|
||||
|
||||
pub async fn setup_user(&self, password: &str) -> Result<()> {
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
|
||||
|
||||
let password_hash = hash(password, DEFAULT_COST)?;
|
||||
|
||||
|
||||
// If onboarding was already completed (before setup), preserve that
|
||||
let onboarding_complete = self.is_onboarding_complete().await?;
|
||||
|
||||
let user = User {
|
||||
password_hash,
|
||||
setup_complete: true,
|
||||
onboarding_complete: false,
|
||||
onboarding_complete,
|
||||
};
|
||||
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
@@ -60,6 +68,15 @@ impl AuthManager {
|
||||
}
|
||||
|
||||
pub async fn complete_onboarding(&self) -> Result<()> {
|
||||
// Persist to onboarding.json (works even before user/setup exists)
|
||||
let onboarding_file = self.data_dir.join("onboarding.json");
|
||||
let state = OnboardingState { complete: true };
|
||||
fs::write(
|
||||
&onboarding_file,
|
||||
serde_json::to_string_pretty(&state)?,
|
||||
)
|
||||
.await?;
|
||||
// Also update user.json if it exists (keeps them in sync)
|
||||
if let Some(mut user) = self.get_user().await? {
|
||||
user.onboarding_complete = true;
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
@@ -69,6 +86,25 @@ impl AuthManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_onboarding_complete(&self) -> Result<bool> {
|
||||
// Check onboarding.json first (persisted before user setup)
|
||||
let onboarding_file = self.data_dir.join("onboarding.json");
|
||||
if onboarding_file.exists() {
|
||||
let content = fs::read_to_string(&onboarding_file).await?;
|
||||
if let Ok(state) = serde_json::from_str::<OnboardingState>(&content) {
|
||||
if state.complete {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: user.json
|
||||
Ok(self
|
||||
.get_user()
|
||||
.await?
|
||||
.map(|u| u.onboarding_complete)
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
pub async fn verify_password(&self, password: &str) -> Result<bool> {
|
||||
use bcrypt::verify;
|
||||
|
||||
@@ -78,4 +114,113 @@ impl AuthManager {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Change password: verify current, validate new, update user.json and optionally SSH.
|
||||
/// New password must be 12+ chars with upper, lower, digit, and special character.
|
||||
pub async fn change_password(
|
||||
&self,
|
||||
current_password: &str,
|
||||
new_password: &str,
|
||||
also_change_ssh: bool,
|
||||
) -> Result<()> {
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
|
||||
if !self.verify_password(current_password).await? {
|
||||
anyhow::bail!("Current password is incorrect");
|
||||
}
|
||||
|
||||
validate_password_strength(new_password)?;
|
||||
|
||||
let password_hash = hash(new_password, DEFAULT_COST)?;
|
||||
|
||||
let mut user = self
|
||||
.get_user()
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
|
||||
|
||||
user.password_hash = password_hash;
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
let content = serde_json::to_string_pretty(&user)?;
|
||||
fs::write(&user_file, content).await?;
|
||||
|
||||
if also_change_ssh {
|
||||
change_ssh_password(new_password).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate password strength: 12+ chars, upper, lower, digit, special.
|
||||
fn validate_password_strength(password: &str) -> Result<()> {
|
||||
if password.len() < 12 {
|
||||
anyhow::bail!("Password must be at least 12 characters");
|
||||
}
|
||||
if !password.chars().any(|c| c.is_ascii_uppercase()) {
|
||||
anyhow::bail!("Password must contain at least one uppercase letter");
|
||||
}
|
||||
if !password.chars().any(|c| c.is_ascii_lowercase()) {
|
||||
anyhow::bail!("Password must contain at least one lowercase letter");
|
||||
}
|
||||
if !password.chars().any(|c| c.is_ascii_digit()) {
|
||||
anyhow::bail!("Password must contain at least one digit");
|
||||
}
|
||||
if !password.chars().any(|c| !c.is_ascii_alphanumeric()) {
|
||||
anyhow::bail!("Password must contain at least one special character (!@#$%^&* etc.)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change the archipelago user's SSH/login password.
|
||||
/// Uses usermod + openssl to bypass PAM (avoids "Authentication token manipulation" errors).
|
||||
/// Uses absolute paths (/usr/bin/openssl, /usr/sbin/usermod) for systemd's minimal PATH.
|
||||
async fn change_ssh_password(new_password: &str) -> Result<()> {
|
||||
let ssh_user = std::env::var("ARCHIPELAGO_SSH_USER").unwrap_or_else(|_| "archipelago".to_string());
|
||||
|
||||
// Generate crypt hash via openssl (SHA-512, compatible with /etc/shadow)
|
||||
// Use /usr/bin/openssl - systemd services often have minimal PATH
|
||||
let mut hash_child = tokio::process::Command::new("/usr/bin/openssl")
|
||||
.args(["passwd", "-6", "-stdin"])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to run openssl: {}. Is openssl installed?", e))?;
|
||||
|
||||
{
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut stdin = hash_child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to open openssl stdin"))?;
|
||||
stdin.write_all(new_password.as_bytes()).await?;
|
||||
stdin.flush().await?;
|
||||
}
|
||||
|
||||
let hash_result = hash_child.wait_with_output().await?;
|
||||
if !hash_result.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&hash_result.stderr);
|
||||
anyhow::bail!("openssl passwd failed: {}", stderr);
|
||||
}
|
||||
let hash = String::from_utf8(hash_result.stdout)?
|
||||
.trim()
|
||||
.to_string();
|
||||
if hash.is_empty() {
|
||||
anyhow::bail!("openssl passwd produced empty hash");
|
||||
}
|
||||
|
||||
// usermod -p writes directly to /etc/shadow, bypassing PAM
|
||||
// Use /usr/sbin/usermod - not always in systemd's PATH
|
||||
let status = tokio::process::Command::new("/usr/sbin/usermod")
|
||||
.args(["-p", &hash, &ssh_user])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !status.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&status.stderr);
|
||||
anyhow::bail!("usermod failed: {}", stderr);
|
||||
}
|
||||
|
||||
tracing::info!("SSH password updated for user {}", ssh_user);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user