v1.0: First production version

This first version allows login of pre-existing users, creation and update of truths
by admins, vote on the truths by users, their display as well as a simple graph
for the vote results.
Everything persisting in a SQLite database.
This commit is contained in:
trotFunky 2024-07-23 21:51:42 +01:00
commit 9911895b5b
22 changed files with 4790 additions and 0 deletions

201
src/auth.rs Normal file
View file

@ -0,0 +1,201 @@
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 sqlx::Error;
use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote};
use crate::database;
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| {println!("Token : {:?}\nCookie : {:?}", auth_token.token, token_str); 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)
};
let votes: Vec<Vote> = if logged_in {
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("/login", data="<form>")]
pub async fn login(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!("/"));
}
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!("/"));
}
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!("/"));
}
}
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!("/"));
}
}
Redirect::to(uri!("/"))
}
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}");
}