FabulaVotes/src/auth.rs
trotFunky a0b79a17ea Redirects: properly redirect to the current week
Previously, most redirects targeted the root of the application.
This was okay for the first part of development where only one week would be
active, but would be annoying when using multiple weeks.

Change those redirects to call week::week.

Change the login path to be dependant on the current week as well,
so it can be correctly redirected.
2024-07-26 01:08:47 +01:00

201 lines
No EOL
7.7 KiB
Rust

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<Vote>
}
pub async fn get_user(week: u8, db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User {
let auth_token: Option<String> = match cookies.get_private("auth_token") {
Some(cookie) => Some(cookie.value().to_string()),
None => None
};
let auth_id: Option<String> = 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::<AuthTokens> = 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::<AuthTokens>::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<Vote> = 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::<Vote>::new()
})
} else {
Vec::<Vote>::new()
};
if logged_in {
User {
logged_in,
is_admin,
id: id_str.parse::<u16>().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("/<week>/login", data="<form>")]
pub async fn login(week: u8, form: Form<AuthForm>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user_search: Result<PlayerLoginInfo, _> = 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}");
}