387 lines
10 KiB
Rust
387 lines
10 KiB
Rust
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<RegisterRequest>, 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<LoginRequest>, 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<String>,
|
|
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),
|
|
);
|
|
}
|