use actix::Addr; use actix_identity::Identity; use actix_web::{HttpRequest, HttpResponse, Responder, web}; use askama::Template; use crate::actors::{bruteforce_actor, users_actor}; use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor}; use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; use crate::controllers::base_controller::{FatalErrorPage, redirect_user}; use crate::data::app_config::AppConfig; use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::utils::network_utils::{get_remote_ip, parse_ip}; #[derive(Template)] #[template(path = "base_login_page.html")] struct BaseLoginPage { danger: String, success: String, page_title: &'static str, app_name: &'static str, redirect_uri: String, } #[derive(Template)] #[template(path = "login.html")] struct LoginTemplate { _parent: BaseLoginPage, login: String, } #[derive(Template)] #[template(path = "password_reset.html")] struct PasswordResetTemplate { _parent: BaseLoginPage, min_pass_len: usize, } #[derive(serde::Deserialize)] pub struct LoginRequestBody { login: String, password: String, } #[derive(serde::Deserialize)] pub struct LoginRequestQuery { logout: Option, redirect: Option, } /// Authenticate user pub async fn login_route( http_req: HttpRequest, users: web::Data>, bruteforce: web::Data>, query: web::Query, req: Option>, config: web::Data, id: Identity, ) -> impl Responder { let mut danger = String::new(); let mut success = String::new(); let mut login = String::new(); let remote_ip = match parse_ip(&get_remote_ip(&http_req, config.proxy_ip.as_deref())) { None => return HttpResponse::InternalServerError().body( FatalErrorPage { message: "Failed to determine remote ip address!" }.render().unwrap() ), Some(i) => i, }; let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip }) .await.unwrap(); if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS { return HttpResponse::TooManyRequests().body( FatalErrorPage { message: "Too many failed login attempts, please try again later!" }.render().unwrap() ); } let redirect_uri = match query.redirect.as_deref() { None => "/", Some(s) => match s.starts_with('/') && !s.starts_with("//") { true => s, false => "/", }, }; // Check if user session must be closed if let Some(true) = query.logout { id.forget(); success = "Goodbye!".to_string(); } // Check if user is already authenticated if SessionIdentity(&id).is_authenticated() { return redirect_user(redirect_uri); } // Check if user is setting a new password if let (Some(req), true) = (&req, SessionIdentity(&id).need_new_password()) { if req.password.len() < MIN_PASS_LEN { danger = "Password is too short!".to_string(); } else { let res: ChangePasswordResult = users .send(users_actor::ChangePasswordRequest { user_id: SessionIdentity(&id).user_id(), new_password: req.password.clone(), temporary: false, }) .await .unwrap(); if !res.0 { danger = "Failed to change password!".to_string(); } else { SessionIdentity(&id).set_status(SessionStatus::SignedIn); return redirect_user(redirect_uri); } } } // Try to authenticate user else if let Some(req) = &req { login = req.login.clone(); let response: LoginResult = users .send(users_actor::LoginRequest { login: login.clone(), password: req.password.clone(), }) .await .unwrap(); match response { LoginResult::Success(user) => { SessionIdentity(&id).set_user(&user); if user.need_reset_password { SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); } else { return redirect_user(redirect_uri); } } LoginResult::AccountDisabled => { log::warn!("Failed login for username {} : account is disabled", login); danger = "Your account is disabled!".to_string(); } c => { log::warn!("Failed login for ip {:?} / username {}: {:?}", remote_ip, login, c); danger = "Login failed.".to_string(); bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip }).await.unwrap(); } } } // Display password reset form if it is appropriate if SessionIdentity(&id).need_new_password() { return HttpResponse::Ok().content_type("text/html").body( PasswordResetTemplate { _parent: BaseLoginPage { page_title: "Password reset", danger, success, app_name: APP_NAME, redirect_uri: urlencoding::encode(redirect_uri).to_string(), }, min_pass_len: MIN_PASS_LEN, } .render() .unwrap(), ); } HttpResponse::Ok().content_type("text/html").body( LoginTemplate { _parent: BaseLoginPage { page_title: "Login", danger, success, app_name: APP_NAME, redirect_uri: urlencoding::encode(redirect_uri).to_string(), }, login, } .render() .unwrap(), ) } /// Sign out user pub async fn logout_route() -> impl Responder { redirect_user("/login?logout=true") }