312 lines
9.2 KiB
Rust
312 lines
9.2 KiB
Rust
use crate::constants::StaticConstraints;
|
|
use crate::controllers::HttpResult;
|
|
use crate::models::{User, UserID};
|
|
use crate::services::login_token_service::LoginTokenValue;
|
|
use crate::services::rate_limiter_service::RatedAction;
|
|
use crate::services::{login_token_service, openid_service, rate_limiter_service, users_service};
|
|
use actix_remote_ip::RemoteIP;
|
|
use actix_web::{web, HttpResponse};
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct CreateAccountBody {
|
|
name: String,
|
|
email: String,
|
|
}
|
|
|
|
/// Create a new account
|
|
pub async fn create_account(remote_ip: RemoteIP, req: web::Json<CreateAccountBody>) -> HttpResult {
|
|
// Rate limiting
|
|
if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::CreateAccount).await? {
|
|
return Ok(HttpResponse::TooManyRequests().finish());
|
|
}
|
|
|
|
// Check if email is valid
|
|
if !mailchecker::is_valid(&req.email) {
|
|
return Ok(HttpResponse::BadRequest().json("Email address is invalid!"));
|
|
}
|
|
|
|
// Check parameters
|
|
let constraints = StaticConstraints::default();
|
|
if !constraints.user_name_len.validate(&req.name) || !constraints.mail_len.validate(&req.email)
|
|
{
|
|
return Ok(HttpResponse::BadRequest().json("Size constraints were not respected!"));
|
|
}
|
|
|
|
rate_limiter_service::record_action(remote_ip.0, RatedAction::CreateAccount).await?;
|
|
|
|
// Perform cleanup
|
|
users_service::delete_not_validated_accounts().await?;
|
|
|
|
// Check if email is already attached to an account
|
|
if users_service::exists_email(&req.email).await? {
|
|
return Ok(
|
|
HttpResponse::Conflict().json("An account with the same email address already exists!")
|
|
);
|
|
}
|
|
|
|
// Create the account
|
|
let mut user = users_service::create_account(&req.name, &req.email).await?;
|
|
|
|
// Trigger reset password (send mail)
|
|
users_service::request_reset_password(&mut user).await?;
|
|
|
|
// Account successfully created
|
|
Ok(HttpResponse::Created().finish())
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct RequestResetPasswordBody {
|
|
mail: String,
|
|
}
|
|
|
|
/// Request the creation of a new password reset link
|
|
pub async fn request_reset_password(
|
|
remote_ip: RemoteIP,
|
|
req: web::Json<RequestResetPasswordBody>,
|
|
) -> HttpResult {
|
|
// Rate limiting
|
|
if rate_limiter_service::should_block_action(
|
|
remote_ip.0,
|
|
RatedAction::RequestNewPasswordResetLink,
|
|
)
|
|
.await?
|
|
{
|
|
return Ok(HttpResponse::TooManyRequests().finish());
|
|
}
|
|
rate_limiter_service::record_action(remote_ip.0, RatedAction::RequestNewPasswordResetLink)
|
|
.await?;
|
|
|
|
match users_service::get_by_mail(&req.mail).await {
|
|
Ok(mut user) => users_service::request_reset_password(&mut user).await?,
|
|
Err(e) => {
|
|
log::error!(
|
|
"Could not locate user account {}! (error silently ignored)",
|
|
e
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(HttpResponse::Created().finish())
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct CheckResetPasswordTokenBody {
|
|
token: String,
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
pub struct CheckResetPasswordTokenResponse {
|
|
name: String,
|
|
}
|
|
|
|
/// Check reset password token
|
|
pub async fn check_reset_password_token(
|
|
remote_ip: RemoteIP,
|
|
req: web::Json<CheckResetPasswordTokenBody>,
|
|
) -> HttpResult {
|
|
// Rate limiting
|
|
if rate_limiter_service::should_block_action(
|
|
remote_ip.0,
|
|
RatedAction::CheckResetPasswordTokenFailed,
|
|
)
|
|
.await?
|
|
{
|
|
return Ok(HttpResponse::TooManyRequests().finish());
|
|
}
|
|
|
|
let user = match users_service::get_by_pwd_reset_token(&req.token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
rate_limiter_service::record_action(
|
|
remote_ip.0,
|
|
RatedAction::CheckResetPasswordTokenFailed,
|
|
)
|
|
.await?;
|
|
log::error!("Password reset token could not be used: {}", e);
|
|
return Ok(HttpResponse::NotFound().finish());
|
|
}
|
|
};
|
|
|
|
Ok(HttpResponse::Ok().json(CheckResetPasswordTokenResponse { name: user.name }))
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct ResetPasswordBody {
|
|
token: String,
|
|
password: String,
|
|
}
|
|
|
|
/// Reset password
|
|
pub async fn reset_password(remote_ip: RemoteIP, req: web::Json<ResetPasswordBody>) -> HttpResult {
|
|
// Rate limiting
|
|
if rate_limiter_service::should_block_action(
|
|
remote_ip.0,
|
|
RatedAction::CheckResetPasswordTokenFailed,
|
|
)
|
|
.await?
|
|
{
|
|
return Ok(HttpResponse::TooManyRequests().finish());
|
|
}
|
|
|
|
let mut user = match users_service::get_by_pwd_reset_token(&req.token).await {
|
|
Ok(t) => t,
|
|
Err(e) => {
|
|
rate_limiter_service::record_action(
|
|
remote_ip.0,
|
|
RatedAction::CheckResetPasswordTokenFailed,
|
|
)
|
|
.await?;
|
|
log::error!("Password reset token could not be used: {}", e);
|
|
return Ok(HttpResponse::NotFound().finish());
|
|
}
|
|
};
|
|
|
|
if !StaticConstraints::default()
|
|
.password_len
|
|
.validate(&req.password)
|
|
{
|
|
return Ok(HttpResponse::BadRequest().json("Invalid password len!"));
|
|
}
|
|
|
|
// Validate account, if required
|
|
users_service::validate_account(&mut user).await?;
|
|
|
|
// Change user password
|
|
users_service::change_password(&mut user, &req.password).await?;
|
|
|
|
Ok(HttpResponse::Accepted().finish())
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct PasswordLoginQuery {
|
|
mail: String,
|
|
password: String,
|
|
}
|
|
|
|
/// Handle login with password
|
|
pub async fn password_login(remote_ip: RemoteIP, req: web::Json<PasswordLoginQuery>) -> HttpResult {
|
|
// Rate limiting
|
|
if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::FailedPasswordLogin)
|
|
.await?
|
|
{
|
|
return Ok(HttpResponse::TooManyRequests().finish());
|
|
}
|
|
|
|
// Get user account
|
|
let user = match users_service::get_by_mail(&req.mail).await {
|
|
Ok(u) => u,
|
|
Err(e) => {
|
|
log::error!("Auth failed: could not find account by mail! {}", e);
|
|
rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin)
|
|
.await?;
|
|
return Ok(HttpResponse::Unauthorized().json("Invalid credentials"));
|
|
}
|
|
};
|
|
|
|
if !user.check_password(&req.password) {
|
|
log::error!("Auth failed: invalid password for mail {}", user.email);
|
|
rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin).await?;
|
|
return Ok(HttpResponse::Unauthorized().json("Invalid credentials"));
|
|
}
|
|
|
|
finish_login(&user).await
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct LoginResponse {
|
|
user_id: UserID,
|
|
token: String,
|
|
}
|
|
|
|
async fn finish_login(user: &User) -> HttpResult {
|
|
if !user.active {
|
|
log::error!("Auth failed: account for mail {} is disabled!", user.email);
|
|
return Ok(HttpResponse::ExpectationFailed().json("This account is disabled!"));
|
|
}
|
|
|
|
Ok(HttpResponse::Ok().json(LoginResponse {
|
|
user_id: user.id(),
|
|
token: login_token_service::gen_new_token(user).await?,
|
|
}))
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct StartOpenIDLoginQuery {
|
|
provider: String,
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
pub struct StartOpenIDLoginResponse {
|
|
url: String,
|
|
}
|
|
|
|
/// Start OpenID login
|
|
pub async fn start_openid_login(
|
|
remote_ip: RemoteIP,
|
|
req: web::Json<StartOpenIDLoginQuery>,
|
|
) -> HttpResult {
|
|
// Rate limiting
|
|
if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::StartOpenIDLogin).await?
|
|
{
|
|
return Ok(HttpResponse::TooManyRequests().finish());
|
|
}
|
|
rate_limiter_service::record_action(remote_ip.0, RatedAction::StartOpenIDLogin).await?;
|
|
|
|
let url = openid_service::start_login(&req.provider, remote_ip.0).await?;
|
|
|
|
Ok(HttpResponse::Ok().json(StartOpenIDLoginResponse { url }))
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct FinishOpenIDLoginQuery {
|
|
code: String,
|
|
state: String,
|
|
}
|
|
|
|
/// Finish OpenID login
|
|
pub async fn finish_openid_login(
|
|
remote_ip: RemoteIP,
|
|
req: web::Json<FinishOpenIDLoginQuery>,
|
|
) -> HttpResult {
|
|
let user_info = openid_service::finish_login(remote_ip.0, &req.code, &req.state).await?;
|
|
|
|
if user_info.email_verified != Some(true) {
|
|
log::error!("Email is not verified!");
|
|
return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
|
|
}
|
|
|
|
let mail = match user_info.email {
|
|
Some(m) => m,
|
|
None => {
|
|
return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
|
|
}
|
|
};
|
|
|
|
// Create the account, if required
|
|
if !users_service::exists_email(&mail).await? {
|
|
let name = match (user_info.name, user_info.given_name, user_info.family_name) {
|
|
(Some(name), _, _) => name,
|
|
(None, Some(g), Some(f)) => format!("{g} {f}"),
|
|
(_, _, _) => {
|
|
return Ok(HttpResponse::Unauthorized().json("Name unspecified by the IDP!"));
|
|
}
|
|
};
|
|
|
|
users_service::create_account(&name, &mail).await?;
|
|
}
|
|
|
|
let mut user = users_service::get_by_mail(&mail).await?;
|
|
|
|
// OpenID auth is enough to validate accounts
|
|
users_service::validate_account(&mut user).await?;
|
|
|
|
finish_login(&user).await
|
|
}
|
|
|
|
/// Logout user
|
|
pub async fn logout(token: LoginTokenValue) -> HttpResult {
|
|
login_token_service::delete_token(&token).await?;
|
|
|
|
Ok(HttpResponse::NoContent().finish())
|
|
}
|