All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			309 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			309 lines
		
	
	
		
			9.1 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::{HttpResponse, web};
 | |
| 
 | |
| #[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 {e}! (error silently ignored)");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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())
 | |
| }
 |