use std::ops::Deref; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use chrono::{Datelike, Local, NaiveDate}; use log::info; use rocket::{ async_trait, http::{Cookie, Status}, request::{self, FromRequest, Outcome}, time::{Duration, OffsetDateTime}, Request, }; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; use super::{log::Log, tripdetails::TripDetails, Day}; #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct User { pub id: i64, pub name: String, pub pw: Option, pub is_cox: bool, pub is_admin: bool, pub is_guest: bool, pub is_tech: bool, pub deleted: bool, pub last_access: Option, } impl PartialEq for User { fn eq(&self, other: &Self) -> bool { self.id == other.id } } #[derive(Debug)] pub enum LoginError { InvalidAuthenticationCombo, UserNotFound, UserDeleted, NotLoggedIn, NotAnAdmin, NotACox, NotATech, NoPasswordSet(User), DeserializationError, } impl User { pub async fn rowed_km(&self, db: &SqlitePool) -> i32 { sqlx::query!( "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km FROM ( SELECT distance_in_km FROM logbook WHERE shipmaster = ?1 UNION SELECT l.distance_in_km FROM logbook l INNER JOIN rower r ON r.logbook_id = l.id WHERE r.rower_id = ?1 );", self.id, ) .fetch_one(db) .await .unwrap() .rowed_km .unwrap() } pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { sqlx::query_as!( Self, " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE id like ? ", id ) .fetch_one(db) .await .ok() } pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option { sqlx::query_as!( Self, " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE name like ? ", name ) .fetch_one(db) .await .ok() } pub async fn on_water(&self, db: &SqlitePool) -> bool { if sqlx::query!( "SELECT * FROM logbook WHERE shipmaster=? AND arrival is null", self.id ) .fetch_optional(db) .await .unwrap() .is_some() { return true; } if sqlx::query!( "SELECT * FROM logbook JOIN rower ON rower.logbook_id=logbook.id WHERE rower_id=? AND arrival is null", self.id ) .fetch_optional(db) .await .unwrap() .is_some() { return true; } false } pub async fn all(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE deleted = 0 ORDER BY last_access DESC " ) .fetch_all(db) .await .unwrap() } pub async fn cox(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE deleted = 0 AND is_cox=true ORDER BY last_access DESC " ) .fetch_all(db) .await .unwrap() } pub async fn create(db: &SqlitePool, name: &str, is_guest: bool) -> bool { sqlx::query!( "INSERT INTO USER(name, is_guest) VALUES (?,?)", name, is_guest, ) .execute(db) .await .is_ok() } pub async fn update( &self, db: &SqlitePool, is_cox: bool, is_admin: bool, is_guest: bool, is_tech: bool, ) { sqlx::query!( "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ?, is_tech = ? where id = ?", is_cox, is_admin, is_guest, is_tech, self.id ) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result { let name = name.trim(); // just to make sure... let Some(user) = User::find_by_name(db, name).await else { Log::create(db, format!("Username ({name}) not found (tried to login)")).await; return Err(LoginError::InvalidAuthenticationCombo); // Username not found }; if user.deleted { Log::create( db, format!("User ({name}) already deleted (tried to login)."), ) .await; return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has //been deleted } if let Some(user_pw) = user.pw.as_ref() { let password_hash = &Self::get_hashed_pw(pw); if password_hash == user_pw { return Ok(user); } Log::create(db, format!("User {name} supplied the wrong PW")).await; Err(LoginError::InvalidAuthenticationCombo) } else { info!("User {name} has no PW set"); Err(LoginError::NoPasswordSet(user)) } } pub async fn reset_pw(&self, db: &SqlitePool) { sqlx::query!("UPDATE user SET pw = null where id = ?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn update_pw(&self, db: &SqlitePool, pw: &str) { let pw = Self::get_hashed_pw(pw); sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } fn get_hashed_pw(pw: &str) -> String { let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap(); let argon2 = Argon2::default(); argon2 .hash_password(pw.as_bytes(), &salt) .unwrap() .to_string() } pub async fn logged_in(&self, db: &SqlitePool) { sqlx::query!( "UPDATE user SET last_access = CURRENT_TIMESTAMP where id = ?", self.id ) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn delete(&self, db: &SqlitePool) { sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a User of a valid id } pub async fn get_days(&self, db: &SqlitePool) -> Vec { let mut days = Vec::new(); for i in 0..self.amount_days_to_show() { let date = (Local::now() + chrono::Duration::days(i)).date_naive(); if self.is_guest { days.push(Day::new_guest(db, date, false).await); } else { days.push(Day::new(db, date, false).await); } } for date in TripDetails::pinned_days(db, self.amount_days_to_show()).await { if self.is_guest { let day = Day::new_guest(db, date, true).await; if !day.planned_events.is_empty() { days.push(day); } } else { days.push(Day::new(db, date, true).await); } } days } fn amount_days_to_show(&self) -> i64 { if self.is_cox { let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok, //december //has 31 //days end_of_year .signed_duration_since(Local::now().date_naive()) .num_days() + 1 } else { 6 } } } #[async_trait] impl<'r> FromRequest<'r> for User { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match req.cookies().get_private("loggedin_user") { Some(user_id) => match user_id.value().parse::() { Ok(user_id) => { let db = req.rocket().state::().unwrap(); let Some(user) = User::find_by_id(db, user_id).await else { return Outcome::Failure((Status::Unauthorized, LoginError::UserNotFound)); }; if user.deleted { return Outcome::Failure((Status::Unauthorized, LoginError::UserDeleted)); } user.logged_in(db).await; let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); req.cookies().add_private(cookie); Outcome::Success(user) } Err(_) => { println!("{:?}", user_id.value()); Outcome::Failure((Status::Unauthorized, LoginError::DeserializationError)) } }, None => Outcome::Failure((Status::Unauthorized, LoginError::NotLoggedIn)), } } } pub struct TechUser { pub(crate) user: User, } impl Deref for TechUser { type Target = User; fn deref(&self) -> &Self::Target { &self.user } } impl TryFrom for TechUser { type Error = LoginError; fn try_from(user: User) -> Result { if user.is_tech { Ok(TechUser { user }) } else { Err(LoginError::NotATech) } } } #[async_trait] impl<'r> FromRequest<'r> for TechUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match User::from_request(req).await { Outcome::Success(user) => match user.try_into() { Ok(user) => Outcome::Success(user), Err(_) => Outcome::Failure((Status::Unauthorized, LoginError::NotACox)), }, Outcome::Failure(f) => Outcome::Failure(f), Outcome::Forward(f) => Outcome::Forward(f), } } } pub struct CoxUser { user: User, } impl Deref for CoxUser { type Target = User; fn deref(&self) -> &Self::Target { &self.user } } impl TryFrom for CoxUser { type Error = LoginError; fn try_from(user: User) -> Result { if user.is_cox { Ok(CoxUser { user }) } else { Err(LoginError::NotACox) } } } #[async_trait] impl<'r> FromRequest<'r> for CoxUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match User::from_request(req).await { Outcome::Success(user) => match user.try_into() { Ok(user) => Outcome::Success(user), Err(_) => Outcome::Failure((Status::Unauthorized, LoginError::NotACox)), }, Outcome::Failure(f) => Outcome::Failure(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[derive(Debug, Serialize, Deserialize)] pub struct AdminUser { pub(crate) user: User, } impl TryFrom for AdminUser { type Error = LoginError; fn try_from(user: User) -> Result { if user.is_admin { Ok(AdminUser { user }) } else { Err(LoginError::NotAnAdmin) } } } #[async_trait] impl<'r> FromRequest<'r> for AdminUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match User::from_request(req).await { Outcome::Success(user) => match user.try_into() { Ok(user) => Outcome::Success(user), Err(_) => Outcome::Failure((Status::Unauthorized, LoginError::NotAnAdmin)), }, Outcome::Failure(f) => Outcome::Failure(f), Outcome::Forward(f) => Outcome::Forward(f), } } } #[cfg(test)] mod test { use crate::testdb; use super::User; use sqlx::SqlitePool; #[sqlx::test] fn test_find_correct_id() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.id, 1); } #[sqlx::test] fn test_find_wrong_id() { let pool = testdb!(); let user = User::find_by_id(&pool, 1337).await; assert!(user.is_none()); } #[sqlx::test] fn test_find_correct_name() { let pool = testdb!(); let user = User::find_by_name(&pool, "admin".into()).await.unwrap(); assert_eq!(user.id, 1); } #[sqlx::test] fn test_find_wrong_name() { let pool = testdb!(); let user = User::find_by_name(&pool, "name-does-not-exist".into()).await; assert!(user.is_none()); } #[sqlx::test] fn test_all() { let pool = testdb!(); let res = User::all(&pool).await; assert!(res.len() > 3); } #[sqlx::test] fn test_cox() { let pool = testdb!(); let res = User::cox(&pool).await; assert_eq!(res.len(), 3); } #[sqlx::test] fn test_succ_create() { let pool = testdb!(); assert_eq!( User::create(&pool, "new-user-name".into(), false).await, true ); } #[sqlx::test] fn test_duplicate_name_create() { let pool = testdb!(); assert_eq!(User::create(&pool, "admin".into(), false).await, false); } #[sqlx::test] fn test_update() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); user.update(&pool, false, false, false, false).await; let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.is_admin, false); } #[sqlx::test] fn succ_login_with_test_db() { let pool = testdb!(); User::login(&pool, "admin".into(), "admin".into()) .await .unwrap(); } #[sqlx::test] fn wrong_pw() { let pool = testdb!(); assert!(User::login(&pool, "admin".into(), "admi".into()) .await .is_err()); } #[sqlx::test] fn wrong_username() { let pool = testdb!(); assert!(User::login(&pool, "admi".into(), "admin".into()) .await .is_err()); } #[sqlx::test] fn reset() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); user.reset_pw(&pool).await; let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.pw, None); } #[sqlx::test] fn update_pw() { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); assert!(User::login(&pool, "admin".into(), "abc".into()) .await .is_err()); user.update_pw(&pool, "abc".into()).await; User::login(&pool, "admin".into(), "abc".into()) .await .unwrap(); } }