use rocket::http::CookieJar; use rocket::serde::{Deserialize, Serialize}; use rocket::form::Form; use rocket::response::Redirect; use rocket_db_pools::{sqlx, sqlx::Row, Connection}; use std::time::{SystemTime, UNIX_EPOCH}; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use blake2::{Blake2b512, Digest}; use blake2::digest::FixedOutput; use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote}; use crate::{database, week}; use database::Db; // TODO: Make FromRequest guard https://api.rocket.rs/v0.5/rocket/request/trait.FromRequest and split admin #[derive(Deserialize, Serialize)] #[serde(crate = "rocket::serde")] pub struct User { pub logged_in: bool, pub is_admin: bool, pub id: u16, pub name: String, pub has_week_vote: bool, pub votes: Vec } pub async fn get_user(week: u8, db: &mut Connection, cookies: &CookieJar<'_>) -> User { let auth_token: Option = match cookies.get_private("auth_token") { Some(cookie) => Some(cookie.value().to_string()), None => None }; let auth_id: Option = match cookies.get_private("auth_id") { Some(cookie) => Some(cookie.value().to_string()), None => None }; let current_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs(); match sqlx::query("DELETE FROM AuthTokens WHERE max_timestamp < $1;") .bind(current_time as i64) .fetch_optional(&mut ***db).await { Ok(_) => debug!("Cleaned up old tokens"), Err(error) => error!("Error while cleaning up old tokens : {error}"), }; let id_str; let mut logged_in: bool = if auth_token.is_some() && auth_id.is_some() { id_str = auth_id.unwrap().to_string(); let token_str = auth_token.unwrap().to_string(); let tokens: Vec:: = match sqlx::query_as("SELECT token FROM AuthTokens WHERE player_id == $1") .bind(&id_str) .fetch_all(&mut ***db).await { Ok(auth_tokens) => {auth_tokens}, Err(error) => { error!("Failed to fetch auth tokens for {:?} : {error}", &id_str); Vec::::new() } }; tokens.iter().find(|auth_token| {auth_token.token.eq(&token_str)}).is_some() } else { id_str = String::new(); false }; // Consider the user authentified now, but revert in case of an error. let (name, is_admin): (String, bool) = if logged_in { match sqlx::query("SELECT name, is_admin FROM Players WHERE id == $1") .bind(&id_str) .fetch_one(&mut ***db) .await { Ok(row) => match (row.try_get(0).ok().unwrap(), row.try_get(1).ok().unwrap()) { (Some(name), Some(admin)) => (name, admin), _ => { error!("Invalid return to retrieve user name and admin status"); logged_in = false; (String::new(), false) } }, Err(error) => { error!("Error while retrieving user name : {error}"); logged_in = false; (String::new(), false) }, } } else { (String::new(), false) }; // TODO: Move to src/vote.rs let votes: Vec = if logged_in && !is_admin { sqlx::query_as("SELECT Votes.* FROM Votes JOIN Truths ON Votes.truth_id == Truths.id AND Truths.week == $1 WHERE voter_id == $2 ORDER BY Truths.number;") .bind(week) .bind(&id_str) .fetch_all(&mut ***db).await.unwrap_or_else(|error| { error!("Error while getting votes : {error}"); Vec::::new() }) } else { Vec::::new() }; if logged_in { User { logged_in, is_admin, id: id_str.parse::().unwrap(), name, has_week_vote: if votes.is_empty() { false } else { true }, votes } } else { User { logged_in, is_admin: false, id: 0, name, has_week_vote: false, votes } } } #[derive(FromForm)] pub struct AuthForm { name: String, password: String } #[post("//login", data="
")] pub async fn login(week: u8, form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { let user_search: Result = sqlx::query_as("SELECT id, is_admin, name, pwd_hash FROM Players WHERE name == $1") .bind(&form.name) .fetch_one(&mut **db) .await; if user_search.is_err() { error!("Login failed : invalid user {:?}, err: {:?}", form.name, user_search.err()); cookies.add(("toast_error", "Impossible de se connecter !")); return Redirect::to(uri!(week::week(week))); } let new_user = user_search.unwrap(); let password_hash_parse = PasswordHash::new(new_user.pwd_hash.as_str()); if password_hash_parse.is_err() { error!("Login failed : could not parse password hash {:?}", password_hash_parse.err()); cookies.add(("toast_error", "Impossible de se connecter !")); return Redirect::to(uri!(week::week(week))); } let password_hash = password_hash_parse.unwrap(); match Argon2::default().verify_password(form.password.as_bytes(), &password_hash) { Ok(_) => { let token_creation_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs(); // Generate some kind of authentication token. let mut hasher = Blake2b512::new(); hasher.update(new_user.id.to_le_bytes()); hasher.update(token_creation_time.to_le_bytes()); let hash = hasher.finalize_fixed().to_ascii_lowercase(); let hash_str = String::from_utf8_lossy(hash.as_slice()).to_ascii_lowercase(); match sqlx::query("INSERT INTO AuthTokens (player_id, token, max_timestamp) VALUES ($1, $2, $3);") .bind(new_user.id) .bind(hash_str.clone()) .bind((token_creation_time + 3628800) as i64) // Should be a while until we overflow to the 64th bit... .fetch_optional(&mut **db) .await { Ok(_) => {} Err(error) => { error!("Login failed : coult not store auth token in database : {error}"); cookies.add(("toast_error", "Impossible de se connecter !")); return Redirect::to(uri!(week::week(week))); } } cookies.add_private(("auth_token", hash_str.clone())); cookies.add_private(("auth_id", new_user.id.to_string())); } Err(err) => { error!("Login failed : invalid password for {:?}\nError : {err}", new_user.name); cookies.add(("toast_error", "Impossible de se connecter !")); return Redirect::to(uri!(week::week(week))); } } Redirect::to(uri!(week::week(week))) } pub fn bypass_auth_debug(cookies: &CookieJar<'_>) { if cookies.get_private("auth_token").is_some() { return } let mut hasher = Blake2b512::new(); hasher.update(b"8"); hasher.update(SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs().to_le_bytes()); let hash = hasher.finalize_fixed().to_ascii_lowercase(); let hash_str = String::from_utf8_lossy(hash.as_slice()).to_ascii_lowercase(); cookies.add_private(("auth_token", hash_str.clone())); cookies.add_private(("auth_id", 8.to_string())); println!("Generated hash string : {hash_str}"); }