Files
narl-workspace/src/error.rs
T
nvrl 41d2d7ef77 feat: slow down repeated failed logins
The login endpoint answered guesses as fast as they arrived, against a
single static password.

Wrong attempts now back off — 1s, 2s, 4s, up to 30s — and a correct one
clears the count. It is a single counter rather than a table keyed by
address: there is only one account, so an attacker gains nothing by
rotating source IPs, and there is no per-client map to grow. The cap is
there so that someone hammering the login delays the owner by half a
minute rather than locking them out.

Wrong credentials now answer 401 rather than 400. The server also warns
at startup if DIARY_PASSWORD is still the value from .env.example.
2026-09-05 09:47:27 +02:00

50 lines
1.4 KiB
Rust

use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
#[derive(Debug)]
pub enum AppError {
Unauthorized,
NotFound,
BadRequest(String),
/// The caller must wait this many seconds before trying again.
TooManyRequests(i64),
Internal(anyhow::Error),
}
pub type AppResult<T> = Result<T, AppError>;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".to_string()),
AppError::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()),
AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
AppError::TooManyRequests(wait) => (
StatusCode::TOO_MANY_REQUESTS,
format!("too many attempts — wait {wait}s"),
),
AppError::Internal(e) => {
tracing::error!("internal error: {e:#}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal error".to_string(),
)
}
};
(status, Json(json!({ "error": message }))).into_response()
}
}
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
AppError::Internal(err.into())
}
}