From ee7ed481443bc04ef2b16f3a1dd258e509694498 Mon Sep 17 00:00:00 2001 From: jakubmanczak Date: Tue, 24 Feb 2026 00:55:19 +0100 Subject: [PATCH] CompositeError, UserAuthRequired, /users/self & users/:id, misc --- src/api/mod.rs | 38 ++++++++++++++++++++++++-- src/api/users.rs | 24 ++++++++++++++++ src/main.rs | 5 +++- src/users/auth/implementation.rs | 47 ++++++++++++++++++++++++++++---- src/users/auth/mod.rs | 6 ++++ src/users/mod.rs | 44 +++++++++++++++++++++++------- src/users/sessions.rs | 19 ++++++++++++- 7 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 src/api/users.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index a6af73c..17a4a00 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,39 @@ -use axum::{Router, routing::get}; +use axum::{ + Router, + response::{IntoResponse, Response}, + routing::get, +}; + +use crate::{ + api::users::{get_by_id, get_me}, + users::{UserError, auth::AuthError, sessions::SessionError}, +}; + +mod users; pub fn api_router() -> Router { - Router::new().route("/api/live", get(async || "Mnemosyne lives")) + Router::new() + .route("/api/live", get(async || "Mnemosyne lives")) + .route("/api/users/me", get(get_me)) + .route("/api/users/{id}", get(get_by_id)) } + +pub struct CompositeError(Response); +impl IntoResponse for CompositeError { + fn into_response(self) -> Response { + self.0 + } +} + +macro_rules! composite_from { + ($($t:ty),+ $(,)?) => { + $( + impl From<$t> for CompositeError { + fn from(e: $t) -> Self { + CompositeError(e.into_response()) + } + } + )+ + }; +} +composite_from!(AuthError, UserError, SessionError); diff --git a/src/api/users.rs b/src/api/users.rs new file mode 100644 index 0000000..fed6e94 --- /dev/null +++ b/src/api/users.rs @@ -0,0 +1,24 @@ +use axum::{ + Json, + extract::Path, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Response}, +}; +use uuid::Uuid; + +use crate::{ + api::CompositeError, + users::{ + User, + auth::{UserAuthRequired, UserAuthenticate}, + }, +}; + +pub async fn get_me(h: HeaderMap) -> Result { + Ok(Json(User::authenticate(&h)?.required()?).into_response()) +} + +pub async fn get_by_id(Path(id): Path, h: HeaderMap) -> Result { + User::authenticate(&h)?.required()?; + Ok(Json(User::get_by_id(id)?).into_response()) +} diff --git a/src/main.rs b/src/main.rs index 86ec1c6..a83fa51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,12 @@ mod quotes; mod tags; mod users; -// Mnemosyne, the mother of the nine muses +/// Mnemosyne, the mother of the nine muses const DEFAULT_PORT: u16 = 0x9999; // 39321 +/// The string to be returned alongside HTTP 500 +const ISE_MSG: &str = "Internal server error"; + #[tokio::main] async fn main() -> Result<(), Box> { if let Err(e) = dotenvy::dotenv() { diff --git a/src/users/auth/implementation.rs b/src/users/auth/implementation.rs index e2f02f6..cc93bb1 100644 --- a/src/users/auth/implementation.rs +++ b/src/users/auth/implementation.rs @@ -1,17 +1,23 @@ use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString}; -use axum::http::{ - HeaderMap, - header::{AUTHORIZATION, COOKIE}, +use axum::{ + http::{ + HeaderMap, StatusCode, + header::{AUTHORIZATION, COOKIE}, + }, + response::{IntoResponse, Response}, }; use base64::{Engine, prelude::BASE64_STANDARD}; use rusqlite::OptionalExtension; use uuid::Uuid; use crate::{ - database, + ISE_MSG, database, users::{ User, - auth::{AuthError, COOKIE_NAME, TokenSize, UserAuthenticate, UserPasswordHashing}, + auth::{ + AuthError, COOKIE_NAME, TokenSize, UserAuthRequired, UserAuthenticate, + UserPasswordHashing, + }, sessions::Session, }, }; @@ -27,6 +33,37 @@ impl TokenSize { } } +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + match self { + Self::InvalidCredentials => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::AuthRequired => (StatusCode::UNAUTHORIZED, self.to_string()).into_response(), + Self::SessionError(e) => e.into_response(), + Self::UserError(e) => e.into_response(), + Self::InvalidFormat => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::InvalidBase64(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::InvalidUtf8(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::DatabaseError(e) => { + eprintln!("[ERROR] Database error occured: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, ISE_MSG.to_string()).into_response() + } + Self::PassHashError(e) => { + eprintln!("[ERROR] A passwordhash error occured: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, ISE_MSG.to_string()).into_response() + } + } + } +} + +impl UserAuthRequired for Option { + fn required(self) -> Result { + match self { + Self::None => Err(AuthError::AuthRequired), + Self::Some(u) => Ok(u), + } + } +} + impl UserPasswordHashing for User { fn hash_password(passw: &str) -> Result { use rand08::rngs::OsRng as ArgonOsRng; diff --git a/src/users/auth/mod.rs b/src/users/auth/mod.rs index 662a65f..d7841f1 100644 --- a/src/users/auth/mod.rs +++ b/src/users/auth/mod.rs @@ -10,6 +10,10 @@ pub const COOKIE_NAME: &str = "mnemohash"; pub trait UserAuthenticate { fn authenticate(headers: &HeaderMap) -> Result, AuthError>; } +pub trait UserAuthRequired { + fn required(self) -> Result; +} + pub trait UserPasswordHashing { /// Returns the hashed password as a String fn hash_password(passw: &str) -> Result; @@ -21,6 +25,8 @@ pub trait UserPasswordHashing { pub enum AuthError { #[error("Invalid credentials")] InvalidCredentials, + #[error("Authentication required")] + AuthRequired, #[error("Session error: {0}")] SessionError(#[from] SessionError), #[error("User error: {0}")] diff --git a/src/users/mod.rs b/src/users/mod.rs index ad68bb1..36f85ef 100644 --- a/src/users/mod.rs +++ b/src/users/mod.rs @@ -1,8 +1,13 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; use rusqlite::OptionalExtension; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ + ISE_MSG, database::{self}, users::{ auth::{AuthError, UserPasswordHashing}, @@ -34,16 +39,6 @@ pub enum UserError { #[error("Argon2 passhash error: {0}")] PassHashError(argon2::password_hash::Error), } -impl From for UserError { - fn from(error: rusqlite::Error) -> Self { - UserError::DatabaseError(error.to_string()) - } -} -impl From for UserError { - fn from(err: argon2::password_hash::Error) -> Self { - UserError::PassHashError(err) - } -} impl User { pub fn get_by_id(id: Uuid) -> Result { @@ -176,3 +171,32 @@ impl User { self.id == Uuid::nil() } } + +impl From for UserError { + fn from(error: rusqlite::Error) -> Self { + UserError::DatabaseError(error.to_string()) + } +} +impl From for UserError { + fn from(err: argon2::password_hash::Error) -> Self { + UserError::PassHashError(err) + } +} +impl IntoResponse for UserError { + fn into_response(self) -> Response { + match self { + Self::DatabaseError(e) => { + eprintln!("[ERROR] Database error occured: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, ISE_MSG.into()) + } + Self::PassHashError(e) => { + eprintln!("[ERROR] A passwordhash error occured: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, ISE_MSG.into()) + } + Self::UserHandleError(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::NoUserWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::NoUserWithHandle(_) => (StatusCode::BAD_REQUEST, self.to_string()), + } + .into_response() + } +} diff --git a/src/users/sessions.rs b/src/users/sessions.rs index 368795c..cd3f8be 100644 --- a/src/users/sessions.rs +++ b/src/users/sessions.rs @@ -1,3 +1,7 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; use chrono::{DateTime, Duration, Utc}; use rusqlite::OptionalExtension; use serde::{Deserialize, Serialize}; @@ -5,7 +9,7 @@ use sha2::{Digest, Sha256}; use uuid::Uuid; use crate::{ - database, + ISE_MSG, database, users::{User, auth}, }; @@ -44,6 +48,19 @@ impl From for SessionError { SessionError::DatabaseError(error.to_string()) } } +impl IntoResponse for SessionError { + fn into_response(self) -> Response { + match self { + Self::DatabaseError(e) => { + eprintln!("[ERROR] Database error occured: {e}"); + (StatusCode::INTERNAL_SERVER_ERROR, ISE_MSG.into()) + } + Self::NoSessionWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::NoSessionWithToken(_) => (StatusCode::BAD_REQUEST, self.to_string()), + } + .into_response() + } +} impl Session { pub fn get_by_id(id: Uuid) -> Result {