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 mut form: LoginForm = request.form();
let username = form.cleaned_string("username").unwrap_or_default();
let ip = request.headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown")
.to_string();
let key = LoginGuard::effective_key(&username, &ip);
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");
}
if request.is_post() && form.is_valid().await {
let password = form.cleaned_string("password").unwrap_or_default();
let db = &request.engine.db;
match authenticate(&username, &password, db).await {
Some(user) => {
guard.record_success(&key);
auth_login(&request.session, db, user.id).await.ok();
return Ok(Redirect::to("/profile").into_response());
}
None => {
guard.record_failure(&key);
error!(request.notices => "Incorrect credentials.");
}
}
}
context_update!(request => { "login_form" => &form });
request.render("auth/login.html")
}