use actix::Addr; use actix_identity::Identity; use actix_web::{HttpResponse, Responder, web}; use askama::Template; use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor}; use crate::actors::users_actor; use crate::constants::{APP_NAME, MIN_PASS_LEN}; use crate::controllers::base_controller::redirect_user; use crate::data::session_identity::{SessionIdentity, SessionStatus}; #[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(users: web::Data>, query: web::Query, req: Option>, id: Identity) -> impl Responder { let mut danger = String::new(); let mut success = String::new(); let mut login = String::new(); 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 { // TODO : check request origin (check for valid Referer) 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 => { // TODO : add bruteforce detection log::warn!("Failed login for username {} : {:?}", login, c); danger = "Login failed.".to_string(); } } } // 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") }