use std::env; use actix_web::{ get, post, web, HttpResponse, ResponseError, cookie::{Cookie, time::Duration}, HttpRequest, }; use log::error; use redis::AsyncCommands; use siren::ServiceError; use crate::{ auth::{InsertUser, Auth, LoginRequest, QueryUser, RegisterUser, Session, SESSION_COOKIE_NAME}, storage::{self}, }; use super::verify_hash; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { let register_user = user.0; let insert_user: InsertUser = match register_user.convert_to_insert() { Ok(user) => user, Err(err) => return ResponseError::error_response(&err), }; match InsertUser::insert(insert_user) { Ok(_) => HttpResponse::Created().finish(), Err(err) => { // Obfuscate the service error message to prevent leaking database details if err.status == 409 { return HttpResponse::Conflict().finish(); } else { return ResponseError::error_response(&err); } } } } #[post("/login")] async fn login(request: HttpRequest, login_request: web::Json) -> HttpResponse { let email = login_request.email.clone(); // Get IP address let ip_address = request.peer_addr().unwrap().ip().to_string(); let query_user = match QueryUser::get_by_email(&email) { Ok(query_user) => query_user, Err(_) => { return ResponseError::error_response(&ServiceError { status: 401, message: "The email or password was incorrect.".to_string(), }) } }; // Verify password if verify_hash(&login_request.password, &query_user.hash) { let session = Session::new(&email, &ip_address); let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); return ResponseError::error_response(&err); } }; let session_ttl = env::var("SESSION_TTL") .expect("SESSION_TTL must be set") .parse::() .expect("SESSION_TTL must be an integer"); let session_result: redis::RedisResult<()> = conn .set_ex( session.id.to_string(), &serde_json::to_string(&session).unwrap(), (session_ttl * 60) as usize, ) .await; if let Err(err) = session_result { error!("Failed to set access token in redis: {}", err); return ResponseError::error_response(&ServiceError::from(err)); }; let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session.id.clone()) .path("/") .max_age(Duration::new(session_ttl * 60, 0)) .http_only(true) .secure(true) .finish(); let user_id_cookie = Cookie::build("user_id", session.user_id.clone()) .path("/") .max_age(Duration::new(session_ttl * 60, 0)) .http_only(false) .finish(); HttpResponse::Ok() .cookie(session_cookie) .cookie(user_id_cookie) .json(Auth { id: session.id, user: query_user.into(), }) } else { return ResponseError::error_response(&ServiceError { status: 401, message: "The email or password was incorrect.".to_string(), }); } } #[get("/refresh")] async fn refresh(req: HttpRequest, auth: Auth) -> HttpResponse { let ip_address = req.peer_addr().unwrap().ip().to_string(); let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); return ResponseError::error_response(&err); } }; let session_ttl = env::var("SESSION_TTL") .expect("SESSION_TTL must be set") .parse::() .expect("SESSION_TTL must be an integer"); // Delete old session let _: redis::RedisResult<()> = conn.del(auth.id).await; // Create new session let session = Session::new(&auth.user.email, &ip_address); let session_result: redis::RedisResult<()> = conn .set_ex( session.id.to_string(), &serde_json::to_string(&session).unwrap(), (session_ttl * 60) as usize, ) .await; if let Err(err) = session_result { error!("Failed to set session id in redis: {}", err); return ResponseError::error_response(&ServiceError::from(err)); }; // Create cookies let session_cookie = session.create_cookie(); let user_id_cookie = Cookie::build("user_id", session.user_id.clone()) .path("/") .max_age(Duration::new(session_ttl * 60, 0)) .http_only(false) .finish(); HttpResponse::Ok() .cookie(session_cookie) .cookie(user_id_cookie) .json(Auth { id: session.id, user: auth.user, }) } #[post("/logout")] async fn logout(auth: Auth) -> HttpResponse { let mut conn = match storage::redis_async_connection().await { Ok(conn) => conn, Err(err) => { error!("Failed to get redis connection: {}", err); return ResponseError::error_response(&err); } }; let session_result: redis::RedisResult<()> = conn.del(&auth.id.to_string()).await; if let Err(err) = session_result { error!("Failed to remove session id in redis: {}", err); return ResponseError::error_response(&ServiceError::from(err)); }; let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "") .path("/") .max_age(Duration::new(-1, 0)) .secure(true) .http_only(true) .finish(); let user_id_cookie = Cookie::build("user_id", "") .path("/") .max_age(Duration::new(-1, 0)) .http_only(true) .finish(); HttpResponse::Ok() .cookie(session_cookie) .cookie(user_id_cookie) .finish() } #[get("/me")] async fn me(auth: Auth) -> HttpResponse { HttpResponse::Ok().json(auth) } #[get("/roles")] async fn roles() -> HttpResponse { HttpResponse::Ok().json(vec!["admin", "user"]) } pub fn init_routes(config: &mut web::ServiceConfig) { // TODO: Remove this when deploying let r = RegisterUser { email: "admin".to_string(), password: "admin".to_string(), first_name: "Admin".to_string(), last_name: "Admin".to_string(), }; let mut u = r.convert_to_insert().unwrap(); u.role = "admin".to_string(); u.verified = true; let _ = InsertUser::insert(u); config.service( web::scope("auth") .service(register) .service(login) .service(refresh) .service(logout) .service(me) .service(roles), ); }