logging
This commit is contained in:
+29
-6
@@ -6,6 +6,7 @@ use rand_core::{OsRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::FromRow;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{queries, state::{SharedState, SmtpConfig}};
|
||||
|
||||
@@ -26,21 +27,24 @@ pub struct User { pub id: i64, pub nickname: String, pub email: String, pub pass
|
||||
|
||||
pub async fn identity(State(state): State<SharedState>, Json(req): Json<IdentityRequest>) -> Result<Json<IdentityResponse>, AuthError> {
|
||||
let nickname = validate_nickname(&req.nickname)?;
|
||||
debug!(nickname = %nickname, has_session = req.session_token.is_some(), "identity check requested");
|
||||
match find_user_by_nickname(&state, &nickname).await? {
|
||||
None => Ok(Json(IdentityResponse { nickname, registered: false })),
|
||||
None => { debug!(nickname = %nickname, "nickname is available for guest use"); Ok(Json(IdentityResponse { nickname, registered: false })) },
|
||||
Some(user) => {
|
||||
let token = req.session_token.as_deref().ok_or_else(|| AuthError::unauthorized("This nickname is registered. Log in to use it."))?;
|
||||
let current = user_from_token(&state, token).await?.ok_or_else(|| AuthError::unauthorized("Your session has expired. Log in again."))?;
|
||||
if current.id != user.id { return Err(AuthError::unauthorized("This nickname belongs to another account.")); }
|
||||
info!(user_id = user.id, nickname = %user.nickname, "registered identity authorized");
|
||||
Ok(Json(IdentityResponse { nickname: user.nickname, registered: true }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(State(state): State<SharedState>, Json(req): Json<RegisterRequest>) -> Result<(StatusCode, Json<SessionResponse>), AuthError> {
|
||||
if !state.registration_enabled { return Err(AuthError::forbidden("Registration is disabled.")); }
|
||||
if !state.registration_enabled { warn!("registration attempt rejected because registration is disabled"); return Err(AuthError::forbidden("Registration is disabled.")); }
|
||||
let nickname = validate_nickname(&req.nickname)?;
|
||||
let email = validate_email(&req.email)?;
|
||||
info!(nickname = %nickname, email_domain = %email_domain(&email), "registration requested");
|
||||
validate_password(&req.password)?;
|
||||
let nickname_key = normalize(&nickname);
|
||||
let email_key = normalize(&email);
|
||||
@@ -51,31 +55,41 @@ pub async fn register(State(state): State<SharedState>, Json(req): Json<Register
|
||||
.bind(&nickname).bind(nickname_key).bind(&email).bind(email_key).bind(hash).execute(state.db.pool()).await
|
||||
.map_err(AuthError::database)?;
|
||||
let user = find_user_by_nickname(&state, &nickname).await?.ok_or_else(|| AuthError::internal("Failed to create the account."))?;
|
||||
Ok((StatusCode::CREATED, Json(create_session(&state, &user).await?)))
|
||||
let session = create_session(&state, &user).await?;
|
||||
info!(user_id = user.id, nickname = %user.nickname, "account registered and session created");
|
||||
Ok((StatusCode::CREATED, Json(session)))
|
||||
}
|
||||
|
||||
pub async fn login(State(state): State<SharedState>, Json(req): Json<LoginRequest>) -> Result<Json<SessionResponse>, AuthError> {
|
||||
let email = validate_email(&req.email)?;
|
||||
debug!(email_domain = %email_domain(&email), "login requested");
|
||||
let user = find_user_by_email(&state, &email).await?.ok_or_else(|| AuthError::unauthorized("Invalid e-mail address or password."))?;
|
||||
if !verify_password(&user.password_hash, &req.password) { return Err(AuthError::unauthorized("Invalid e-mail address or password.")); }
|
||||
Ok(Json(create_session(&state, &user).await?))
|
||||
if !verify_password(&user.password_hash, &req.password) { warn!(user_id = user.id, "login rejected: invalid password"); return Err(AuthError::unauthorized("Invalid e-mail address or password.")); }
|
||||
let session = create_session(&state, &user).await?;
|
||||
info!(user_id = user.id, nickname = %user.nickname, "login successful");
|
||||
Ok(Json(session))
|
||||
}
|
||||
|
||||
pub async fn me(State(state): State<SharedState>, headers: HeaderMap) -> Result<Json<SessionResponse>, AuthError> {
|
||||
let token = bearer(&headers).ok_or_else(|| AuthError::unauthorized("Not logged in."))?;
|
||||
let user = user_from_token(&state, token).await?.ok_or_else(|| AuthError::unauthorized("Your session has expired."))?;
|
||||
debug!(user_id = user.id, "session validation successful");
|
||||
let expires_at: String = sqlx::query_scalar(queries::get(state.db.kind(), queries::AUTH_SESSION_EXPIRES_AT))
|
||||
.bind(token).fetch_one(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
Ok(Json(SessionResponse { token: token.into(), nickname: user.nickname, email: user.email, expires_at }))
|
||||
}
|
||||
|
||||
pub async fn logout(State(state): State<SharedState>, headers: HeaderMap) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
if let Some(token) = bearer(&headers) { sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_SESSION_BY_TOKEN)).bind(token).execute(state.db.pool()).await.map_err(AuthError::database)?; }
|
||||
if let Some(token) = bearer(&headers) {
|
||||
let result = sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_SESSION_BY_TOKEN)).bind(token).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
info!(rows_affected = result.rows_affected(), "logout processed");
|
||||
} else { debug!("logout requested without an active session"); }
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
pub async fn request_reset(State(state): State<SharedState>, Json(req): Json<ResetRequest>) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
let email = validate_email(&req.email)?;
|
||||
info!(email_domain = %email_domain(&email), "password reset requested");
|
||||
let smtp = state.smtp.as_ref().ok_or_else(|| AuthError::service_unavailable("Password reset is not configured on this server."))?;
|
||||
if let Some(user) = find_user_by_email(&state, &email).await? {
|
||||
let token = random_token();
|
||||
@@ -84,12 +98,16 @@ pub async fn request_reset(State(state): State<SharedState>, Json(req): Json<Res
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_INSERT_RESET_TOKEN))
|
||||
.bind(hash_token(&token)).bind(user.id).bind(expires).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
send_reset(smtp, &user, &token).await?;
|
||||
info!(user_id = user.id, "password reset e-mail sent");
|
||||
} else {
|
||||
debug!(email_domain = %email_domain(&email), "password reset requested for unknown account");
|
||||
}
|
||||
Ok(Json(serde_json::json!({"ok": true, "message": "If the account exists, a reset link has been sent."})))
|
||||
}
|
||||
|
||||
pub async fn confirm_reset(State(state): State<SharedState>, Json(req): Json<ResetConfirmRequest>) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
validate_password(&req.password)?;
|
||||
info!("password reset confirmation requested");
|
||||
let now_time = Utc::now();
|
||||
let now = now_time.to_rfc3339();
|
||||
let token_hash = hash_token(req.token.trim());
|
||||
@@ -100,6 +118,7 @@ pub async fn confirm_reset(State(state): State<SharedState>, Json(req): Json<Res
|
||||
.map_err(|_| AuthError::bad_request("The reset link is invalid or has expired."))?
|
||||
.with_timezone(&Utc);
|
||||
if used_at.is_some() || expires_at <= now_time {
|
||||
warn!(user_id, used = used_at.is_some(), expired = expires_at <= now_time, "password reset token rejected");
|
||||
return Err(AuthError::bad_request("The reset link is invalid or has expired."));
|
||||
}
|
||||
let password_hash = hash_password(&req.password)?;
|
||||
@@ -112,6 +131,7 @@ pub async fn confirm_reset(State(state): State<SharedState>, Json(req): Json<Res
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_SESSIONS_BY_USER))
|
||||
.bind(user_id).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
tx.commit().await.map_err(AuthError::database)?;
|
||||
info!(user_id, "password reset completed and existing sessions revoked");
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
@@ -140,6 +160,7 @@ async fn create_session(state: &SharedState, user: &User) -> Result<SessionRespo
|
||||
let expires_at = (Utc::now() + Duration::days(30)).to_rfc3339();
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_INSERT_SESSION))
|
||||
.bind(&token).bind(user.id).bind(&expires_at).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
debug!(user_id = user.id, expires_at = %expires_at, "authentication session created");
|
||||
Ok(SessionResponse { token, nickname: user.nickname.clone(), email: user.email.clone(), expires_at })
|
||||
}
|
||||
async fn find_user_by_nickname(state: &SharedState, nickname: &str) -> Result<Option<User>, AuthError> {
|
||||
@@ -185,4 +206,6 @@ async fn send_reset(smtp:&SmtpConfig,user:&User,token:&str)->Result<(),AuthError
|
||||
|
||||
pub struct AuthError { status: StatusCode, pub message: String }
|
||||
impl AuthError { fn bad_request(m:&str)->Self{Self{status:StatusCode::BAD_REQUEST,message:m.into()}} fn unauthorized(m:&str)->Self{Self{status:StatusCode::UNAUTHORIZED,message:m.into()}} fn forbidden(m:&str)->Self{Self{status:StatusCode::FORBIDDEN,message:m.into()}} fn conflict(m:&str)->Self{Self{status:StatusCode::CONFLICT,message:m.into()}} fn internal(m:&str)->Self{Self{status:StatusCode::INTERNAL_SERVER_ERROR,message:m.into()}} fn service_unavailable(m:&str)->Self{Self{status:StatusCode::SERVICE_UNAVAILABLE,message:m.into()}} fn database(e:sqlx::Error)->Self{tracing::error!(error=%e,"authentication database error");Self::internal("Database error.")} }
|
||||
|
||||
fn email_domain(email: &str) -> &str { email.rsplit_once('@').map(|(_, domain)| domain).unwrap_or("invalid") }
|
||||
impl axum::response::IntoResponse for AuthError { fn into_response(self)->axum::response::Response{(self.status,Json(serde_json::json!({"error":self.message}))).into_response()} }
|
||||
|
||||
Reference in New Issue
Block a user