use crate::{ account::{SESSION_COOKIE_NAME, Session, verify_hash}, error::Error, smtp, users::{LoginRequest, RegisterRequest, User, UserResponse}, }; use actix_web::{HttpRequest, HttpResponse, ResponseError, get, post, put, web}; use utoipa_actix_web::scope; use utoipa_actix_web::service_config::ServiceConfig; use crate::account::{Auth, csprng}; use crate::users::UpdateUser; #[utoipa::path( tag = "Account", request_body( content = RegisterRequest, content_type = "application/json" ), responses( (status = 200, description = "", body = UserResponse), (status = 409, description = ""), ) )] #[post("/register")] async fn register(user: web::Json, req: HttpRequest) -> HttpResponse { let register_user = user.into_inner(); let email = register_user.email.clone(); let ip_address = req.peer_addr().unwrap().ip().to_string(); let insert_user: User = match register_user.to_user() { Ok(user) => user, Err(err) => return ResponseError::error_response(&err), }; match insert_user.insert().await { Ok(user) => { let user_response: UserResponse = user.into(); log::info!( "Successful user registration [Email: {}] [IP Address: {}]", email, ip_address ); HttpResponse::Created().json(user_response) } Err(err) => { // Obfuscate the service error message to prevent leaking database details if err.status == 409 { log::warn!( "Duplicate user registration attempt [Email: {}] [IP Address: {}]", email, ip_address ); HttpResponse::Conflict().finish() } else { log::error!("Failed to register user [Email: {}]: {}", email, err); ResponseError::error_response(&err) } } } } #[utoipa::path( tag = "Account", request_body( content = LoginRequest, content_type = "application/json" ), responses( (status = 200, description = "", body = UserResponse), ), )] #[post("/login")] async fn login(request: web::Json, req: HttpRequest) -> HttpResponse { let email = &request.email; let ip_address = req.peer_addr().unwrap().ip().to_string(); let query_user = match User::select_by_email(&email).await { Some(query_user) => query_user, None => return HttpResponse::Unauthorized().finish(), }; if verify_hash(&request.password, &query_user.password_hash) { // Create a session let session = Session::default(&query_user.id, &ip_address); let session_cookie = session.cookie(); let session_exp_cookie = session.expiration_cookie(); // Save the session to the database if let Err(err) = session.store().await { log::error!( "Login attempt failure [Email: {}] [IP Address: {}]: {}", email, ip_address, err ); return ResponseError::error_response(&Error::new(500, err.to_string())); } log::info!( "Successful login attempt [Email: {}] [IP Address: {}]", email, ip_address ); let user_response: UserResponse = query_user.into(); HttpResponse::Ok() .cookie(session_cookie) .cookie(session_exp_cookie) .json(user_response) } else { log::error!( "Invalid login attempt [Email: {}] [IP Address: {}]", email, ip_address ); HttpResponse::Unauthorized() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish() } } #[utoipa::path( tag = "Account", responses( (status = 200, description = ""), (status = 401, description = ""), (status = 500, description = ""), ), security( ("session_auth" = []) ) )] #[post("/logout")] async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse { let email = auth.user.email; let ip_address = req.peer_addr().unwrap().ip().to_string(); // Delete the session from the store match req.cookie(SESSION_COOKIE_NAME) { Some(cookie) => { let session_id = cookie.value().to_string(); if let Err(err) = Session::delete(&session_id).await { log::error!( "Logout attempt failure [Email: {}] [IP Address: {}]: {}", email, ip_address, err ); return ResponseError::error_response(&Error::new(500, err.to_string())); } } None => { log::error!( "Invalid logout attempt [Email: {}] [IP Address: {}]", email, ip_address ); return ResponseError::error_response(&Error::new(400, "Invalid session".to_string())); } } log::info!( "Successful logout attempt [Email: {}] [IP Address: {}]", email, ip_address ); HttpResponse::Ok() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish() } #[utoipa::path( tag = "Account", responses( (status = 200, description = "", body = UserResponse), (status = 401, description = ""), ), security( ("session_auth" = []) ) )] #[get("/profile")] async fn get_profile(req: HttpRequest) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); // Verify a session cookie exists match req.cookie(SESSION_COOKIE_NAME) { // Validate the session Some(cookie) => { let session_id = cookie.value().to_string(); let session = match Session::get(&session_id).await { Ok(session) => session, Err(_) => { log::error!( "Invalid profile attempt [Session: {}] [IP Address: {}]", session_id, ip_address ); return HttpResponse::Unauthorized() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish(); } }; let id = &session.user_id; let query_user = match User::select(&id).await { Some(query_user) => query_user, None => { return HttpResponse::Unauthorized() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish(); } }; let user_response: UserResponse = query_user.into(); let session_cookie = session.cookie(); let session_exp_cookie = session.expiration_cookie(); log::info!( "Successful profile attempt [ID: {}] [IP Address: {}]", id, ip_address ); HttpResponse::Ok() .cookie(session_cookie) .cookie(session_exp_cookie) .json(user_response) } None => HttpResponse::Unauthorized() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish(), } } #[utoipa::path( tag = "Account", responses( (status = 200, description = ""), (status = 401, description = ""), ), security( ("session_auth" = []) ) )] #[get("/session")] async fn session_refresh(req: HttpRequest) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); // Verify a session cookie exists match req.cookie(SESSION_COOKIE_NAME) { // Validate the session Some(cookie) => { let session_id = cookie.value().to_string(); let session = match Session::replace(&session_id, &ip_address).await { Ok(session) => session, Err(_) => { log::error!( "Invalid session validate attempt [Session: {}] [IP Address: {}]", session_id, ip_address ); return HttpResponse::Unauthorized() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish(); } }; let id = &session.user_id; let session_cookie = session.cookie(); let session_exp_cookie = session.expiration_cookie(); log::info!( "Successful session validate attempt [ID: {}] [IP Address: {}]", id, ip_address ); HttpResponse::Ok() .cookie(session_cookie) .cookie(session_exp_cookie) .finish() } None => HttpResponse::Unauthorized() .cookie(Session::empty_cookie()) .cookie(Session::empty_expiration_cookie()) .finish(), } } #[utoipa::path( tag = "Account", request_body( content = String, content_type = "application/json" ), responses( (status = 200, description = "", body = UserResponse), (status = 401, description = ""), ), security( ("session_auth" = []) ) )] #[put("/password")] async fn change_password( password: web::Json, req: HttpRequest, auth: Auth, ) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); let id = auth.user.id; if let None = User::select(&id).await { return HttpResponse::Unauthorized().finish(); }; let update_user = UpdateUser { email: None, email_verified: None, password: Some(password.into_inner()), role: None, first_name: None, last_name: None, avatar: None, }; match update_user.update(&id).await { Ok(user) => { let response: UserResponse = user.into(); log::info!( "Successful password change attempt [ID: {}] [IP Address: {}]", &id, ip_address ); HttpResponse::Ok().json(response) } Err(err) => { log::error!( "Invalid password change attempt [ID: {}] [IP Address: {}]: {}", &id, ip_address, err ); ResponseError::error_response(&err) } } } #[utoipa::path( tag = "Account", responses( (status = 200, description = ""), (status = 401, description = ""), ), security( ("session_auth" = []) ) )] #[post("/password/reset")] async fn reset_password(req: HttpRequest, auth: Auth) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); let id = auth.user.id; let email = auth.user.email; let token = csprng(128); match smtp::send_password_reset(&email, &token) { Ok(_) => HttpResponse::Ok().finish(), Err(err) => { log::error!( "Invalid password reset attempt [ID: {}] [IP Address: {}]: {}", &id, ip_address, err ); ResponseError::error_response(&err) } }; HttpResponse::Ok().finish() } pub fn init_routes(config: &mut ServiceConfig) { config.service( scope::scope("/account") .service(register) .service(login) .service(logout) .service(get_profile) .service(session_refresh) .service(change_password) .service(reset_password), ); }