use super::store::HubStore;
use argon2::Argon2;
use argon2::password_hash::phc::PasswordHash;
use argon2::password_hash::{PasswordHasher, PasswordVerifier};
use std::collections::HashMap;
use std::sync::Mutex;
pub const USER: &str = "admin";
pub const MIN_PASSWORD: usize = 10;
const SESSION_HOURS: i64 = 12;
pub const FAILURE_DELAY_MS: u64 = 400;
pub fn is_claimed(hub: &HubStore) -> bool {
hub.setting("admin_password")
.ok()
.flatten()
.is_some_and(|v| !v.is_empty())
}
pub fn set_password(hub: &HubStore, password: &str) -> Result<(), String> {
if password.chars().count() < MIN_PASSWORD {
return Err(format!(
"The password needs at least {MIN_PASSWORD} characters. Length is the only part \
of this a person controls; everything else is done for them."
));
}
let hash = Argon2::default()
.hash_password(password.as_bytes())
.map_err(|e| format!("the password could not be hashed: {e}"))?
.to_string();
hub.set_setting("admin_password", &hash)
.map_err(|e| e.to_string())
}
pub fn verify(hub: &HubStore, password: &str) -> bool {
let Some(stored) = hub.setting("admin_password").ok().flatten() else {
return false;
};
let Ok(parsed) = PasswordHash::new(&stored) else {
return false;
};
Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok()
}
#[derive(Debug, Clone, PartialEq)]
pub enum Who {
Admin,
Principal {
id: String,
name: String,
role: super::access::Role,
},
}
#[derive(Default)]
pub struct Sessions {
open: Mutex<HashMap<String, (jiff::Timestamp, Who)>>,
}
impl Sessions {
pub fn open(&self, now: jiff::Timestamp) -> String {
self.open_as(now, Who::Admin)
}
pub fn open_as(&self, now: jiff::Timestamp, who: Who) -> String {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes).expect("the operating system has randomness");
let token: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
if let Ok(mut open) = self.open.lock() {
open.retain(|_, (until, _)| *until > now);
open.insert(
token.clone(),
(now + jiff::Span::new().hours(SESSION_HOURS), who),
);
}
token
}
pub fn who(&self, token: &str, now: jiff::Timestamp) -> Option<Who> {
let mut open = self.open.lock().ok()?;
let (until, who) = open.get(token)?.clone();
if until <= now {
return None;
}
open.insert(
token.to_string(),
(now + jiff::Span::new().hours(SESSION_HOURS), who.clone()),
);
Some(who)
}
pub fn close(&self, token: &str) {
if let Ok(mut open) = self.open.lock() {
open.remove(token);
}
}
}
pub const COOKIE: &str = "cyberbrain_hub";
pub fn set_cookie(token: &str, encrypted: bool) -> String {
format!(
"{COOKIE}={token}; Path=/; HttpOnly; SameSite=Strict{}",
if encrypted { "; Secure" } else { "" }
)
}
pub fn clear_cookie(encrypted: bool) -> String {
format!(
"{COOKIE}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0{}",
if encrypted { "; Secure" } else { "" }
)
}
pub fn cookie_from(header: Option<&str>) -> Option<String> {
header?.split(';').find_map(|part| {
let (k, v) = part.trim().split_once('=')?;
(k == COOKIE).then(|| v.to_string())
})
}