Files
siren/service/src/auth/routes.rs
Benjamin Sherriff 1de68f86ae Formatting code
2024-05-12 09:05:59 -04:00

227 lines
6.2 KiB
Rust

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<RegisterUser>) -> 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<LoginRequest>) -> 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::<i64>()
.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::<i64>()
.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),
);
}