//! The first administrator (oxygenhub#245). A hub with no accounts gives //! administrator to the first account registered, so that account must //! bring a one-time setup code: the one in `[registration] setup_code` //! (for automation), or else a random one this module keeps in //! `/setup-code` (owner-only) and logs at start. It is never served //! over the web. Registering with it spends it; once there are accounts //! there is no code. use crate::db::accounts; use crate::Shared; use std::path::PathBuf; pub fn path(app: &Shared) -> PathBuf { app.config.data.join("setup-code") } /// At start: with no accounts, make sure there is a code and say where it /// is; with accounts, make sure none is left lying about. pub async fn ensure(app: &Shared) -> anyhow::Result<()> { let file = path(app); if accounts::account_count(&app.db).await? > 0 { let _ = std::fs::remove_file(&file); return Ok(()); } let code = match expected(app) { Some(code) => code, None => { let code = crate::auth::random_hex(12); write_private(&file, &code)?; code } }; tracing::warn!( "this hub has no administrator yet: register the first account at {}/register with setup code {code} (also in {}); it works once", app.config.base_url(), file.display() ); Ok(()) } /// The code the first registration must bring, if there is one. pub fn expected(app: &Shared) -> Option { let configured = app.config.registration.setup_code.trim(); if !configured.is_empty() { return Some(configured.to_string()); } std::fs::read_to_string(path(app)).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) } /// Whether `given` is the setup code, compared in constant time. pub fn matches(app: &Shared, given: &str) -> bool { match expected(app) { Some(code) => { let (a, b) = (code.as_bytes(), given.trim().as_bytes()); a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 } None => false, } } /// The first administrator exists: the code is spent. pub fn spend(app: &Shared) { let _ = std::fs::remove_file(path(app)); } fn write_private(file: &std::path::Path, code: &str) -> std::io::Result<()> { use std::io::Write; let mut options = std::fs::OpenOptions::new(); options.write(true).create(true).truncate(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600); } let mut f = options.open(file)?; writeln!(f, "{code}") }