227 lines
6.2 KiB
Rust
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),
|
|
);
|
|
}
|