use std::{future::{ready, Ready}, env}; use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http}; use diesel::prelude::*; use log::error; use redis::Commands; use serde::{Serialize, Deserialize}; use siren::ServiceError; use crate::storage::{schema::users, connection}; use super::{hash, AccessToken}; #[derive(Debug, Serialize, Deserialize)] pub struct RegisterUser { pub email: String, pub password: String, pub first_name: String, pub last_name: String, } impl RegisterUser { pub fn convert_to_insert(self) -> Result { Ok(InsertUser { email: self.email.to_lowercase(), hash: hash(&self.password)?, role: "user".to_string(), first_name: self.first_name, last_name: self.last_name, updated_at: chrono::Utc::now().naive_utc(), created_at: chrono::Utc::now().naive_utc(), profile_picture: None, verified: false, }) } } #[derive(Debug, Serialize, Deserialize)] pub struct LoginRequest { pub email: String, pub password: String, } #[derive(Debug, Queryable, QueryableByName, Serialize, Deserialize)] #[diesel(table_name = users)] pub struct QueryUser { pub email: String, pub hash: String, pub role: String, pub first_name: String, pub last_name: String, pub updated_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime, pub profile_picture: Option, pub verified: bool, } impl QueryUser { pub fn get_by_email(email: &str) -> Result { let mut conn = connection()?; // Check if the user exists by email, case insensitive let user = users::table .filter(users::email.eq(email.to_lowercase())) .first(&mut conn)?; Ok(user) } } #[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)] #[diesel(table_name = users)] pub struct InsertUser { pub email: String, pub hash: String, pub role: String, pub first_name: String, pub last_name: String, pub updated_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime, pub profile_picture: Option, pub verified: bool, } impl InsertUser { pub fn insert(user: Self) -> Result { let mut conn = connection()?; let user = diesel::insert_into(users::table) .values(user) .get_result(&mut conn)?; Ok(user) } pub fn update_profile(email: &str, profile_picture: Option<&str>) -> Result { let mut conn = connection()?; let user = diesel::update(users::table) .filter(users::email.eq(&email)) .set(users::profile_picture.eq(profile_picture)) .get_result(&mut conn)?; Ok(user) } } #[derive(Debug, Serialize, Deserialize)] pub struct ResponseUser { pub email: String, pub role: String, pub first_name: String, pub last_name: String, pub profile_picture: Option, } impl From for ResponseUser { fn from(user: QueryUser) -> Self { ResponseUser { email: user.email, role: user.role, first_name: user.first_name, last_name: user.last_name, profile_picture: user.profile_picture, } } } #[derive(Debug, Serialize, Deserialize)] pub struct JwtAuth { pub id: String, pub user: ResponseUser } impl FromRequest for JwtAuth { type Error = ActixError; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let access_token_string = match req .cookie("access_token") .map(|c| c.value().to_string()) .or_else(|| { req.headers().get(http::header::AUTHORIZATION) .map(|h| h.to_str().unwrap().split_at(7).1.to_string()) }) { Some(token) => token, None => return ready(Err(ActixError::from(ServiceError { status: 401, message: "Unauthorized".to_string() }))) }; let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); let public_key = std::fs::read_to_string(format!("{}/public_key.pem", keys_dir)).expect("Failed to read access public key"); let access_token = match AccessToken::decode(&access_token_string, &public_key) { Ok(token_details) => token_details, Err(err) => { error!("Failed to verify access token: {}", err); return ready(Err(ActixError::from(ServiceError { status: 401, message: format!("Access token is invaid: {}", err) }))) } }; let mut conn = match crate::storage::redis_connection() { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); return ready(Err(ActixError::from(ServiceError { status: 500, message: format!("Failed to get redis connection: {}", err) }))) } }; let user_email = match conn.get::<_, String>(access_token.id.clone().to_string()) { Ok(result) => serde_json::from_str::(&result).unwrap().email, Err(_) => { return ready(Err(ActixError::from(ServiceError { status: 401, message: format!("Access token is invalid") }))) } }; match QueryUser::get_by_email(&user_email) { Ok(user) => { ready(Ok(JwtAuth { id: access_token.id, user: user.into() })) } Err(_) => return ready(Err(ActixError::from(ServiceError { status: 401, message: format!("User does not exist") }))) } } } pub fn verify_role(auth: &JwtAuth, role: &str) -> Result<(), ServiceError> { if auth.user.role == role { Ok(()) } else { Err(ServiceError { status: 403, message: "Forbidden".to_string() }) } }