Login Guard

Protection against repeated login attempts.

● ● ●
use runique::prelude::*;
use std::sync::Arc;

// In main() or in the routes module
let guard = Arc::new(
    LoginGuard::new()
        .max_attempts(5)    // attempts before lockout
        .lockout_secs(300), // 5-minute lockout
);

// Clean up expired entries regularly
guard.spawn_cleanup(tokio::time::Duration::from_secs(60));
pub async fn login(
    State(guard): State<Arc<LoginGuard>>,  // State before Request
    mut request: Request,
) -> AppResult<Response> {
    let form: LoginForm = request.form();
    let username = form.cleaned_string("username").unwrap_or_default();
    // Keyed by username: locks the targeted account, whatever the IP
    let key = LoginGuard::effective_key(&username, "unknown");

    if guard.is_locked(&key) {
        if let Some(secs) = guard.remaining_lockout_secs(&key) {
            warning!(request.notices => format!("Account locked. Try again in {secs}s."));
        }
        context_update!(request => { "login_form" => &form });
        return request.render("auth/login.html");
    }

    let validated = match ValidationForm::try_new(form, &request).await {
        Ok(validated) => validated,
        Err(form) => {
            context_update!(request => { "login_form" => &form });
            return request.render("auth/login.html");
        }
    };
    let password = validated.cleaned_string("password").unwrap_or_default();
    let db = &request.engine.db;
    match authenticate_user(db, &username, &password).await {
        Some(user) => {
            guard.record_success(&key);
            if login(&request.session, &user, None, false).await.is_ok() {
                return Ok(Redirect::to("/profile").into_response());
            }
        }
        None => {
            guard.record_failure(&key);
            error!(request.notices => "Incorrect credentials.");
        }
    }
    context_update!(request => { "login_form" => &*validated });
    request.render("auth/login.html")
}

// The admin has its own: .with_admin(|a| a.with_login_guard(LoginGuard::new()))