CompositeError, UserAuthRequired, /users/self & users/:id, misc
This commit is contained in:
@@ -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 {
|
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);
|
||||||
|
|||||||
24
src/api/users.rs
Normal file
24
src/api/users.rs
Normal file
@@ -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<Response, CompositeError> {
|
||||||
|
Ok(Json(User::authenticate(&h)?.required()?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_id(Path(id): Path<Uuid>, h: HeaderMap) -> Result<Response, CompositeError> {
|
||||||
|
User::authenticate(&h)?.required()?;
|
||||||
|
Ok(Json(User::get_by_id(id)?).into_response())
|
||||||
|
}
|
||||||
@@ -9,9 +9,12 @@ mod quotes;
|
|||||||
mod tags;
|
mod tags;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
// Mnemosyne, the mother of the nine muses
|
/// Mnemosyne, the mother of the nine muses
|
||||||
const DEFAULT_PORT: u16 = 0x9999; // 39321
|
const DEFAULT_PORT: u16 = 0x9999; // 39321
|
||||||
|
|
||||||
|
/// The string to be returned alongside HTTP 500
|
||||||
|
const ISE_MSG: &str = "Internal server error";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
if let Err(e) = dotenvy::dotenv() {
|
if let Err(e) = dotenvy::dotenv() {
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
||||||
use axum::http::{
|
use axum::{
|
||||||
HeaderMap,
|
http::{
|
||||||
header::{AUTHORIZATION, COOKIE},
|
HeaderMap, StatusCode,
|
||||||
|
header::{AUTHORIZATION, COOKIE},
|
||||||
|
},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database,
|
ISE_MSG, database,
|
||||||
users::{
|
users::{
|
||||||
User,
|
User,
|
||||||
auth::{AuthError, COOKIE_NAME, TokenSize, UserAuthenticate, UserPasswordHashing},
|
auth::{
|
||||||
|
AuthError, COOKIE_NAME, TokenSize, UserAuthRequired, UserAuthenticate,
|
||||||
|
UserPasswordHashing,
|
||||||
|
},
|
||||||
sessions::Session,
|
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<User> {
|
||||||
|
fn required(self) -> Result<User, AuthError> {
|
||||||
|
match self {
|
||||||
|
Self::None => Err(AuthError::AuthRequired),
|
||||||
|
Self::Some(u) => Ok(u),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl UserPasswordHashing for User {
|
impl UserPasswordHashing for User {
|
||||||
fn hash_password(passw: &str) -> Result<String, argon2::password_hash::Error> {
|
fn hash_password(passw: &str) -> Result<String, argon2::password_hash::Error> {
|
||||||
use rand08::rngs::OsRng as ArgonOsRng;
|
use rand08::rngs::OsRng as ArgonOsRng;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ pub const COOKIE_NAME: &str = "mnemohash";
|
|||||||
pub trait UserAuthenticate {
|
pub trait UserAuthenticate {
|
||||||
fn authenticate(headers: &HeaderMap) -> Result<Option<User>, AuthError>;
|
fn authenticate(headers: &HeaderMap) -> Result<Option<User>, AuthError>;
|
||||||
}
|
}
|
||||||
|
pub trait UserAuthRequired {
|
||||||
|
fn required(self) -> Result<User, AuthError>;
|
||||||
|
}
|
||||||
|
|
||||||
pub trait UserPasswordHashing {
|
pub trait UserPasswordHashing {
|
||||||
/// Returns the hashed password as a String
|
/// Returns the hashed password as a String
|
||||||
fn hash_password(passw: &str) -> Result<String, argon2::password_hash::Error>;
|
fn hash_password(passw: &str) -> Result<String, argon2::password_hash::Error>;
|
||||||
@@ -21,6 +25,8 @@ pub trait UserPasswordHashing {
|
|||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
#[error("Invalid credentials")]
|
#[error("Invalid credentials")]
|
||||||
InvalidCredentials,
|
InvalidCredentials,
|
||||||
|
#[error("Authentication required")]
|
||||||
|
AuthRequired,
|
||||||
#[error("Session error: {0}")]
|
#[error("Session error: {0}")]
|
||||||
SessionError(#[from] SessionError),
|
SessionError(#[from] SessionError),
|
||||||
#[error("User error: {0}")]
|
#[error("User error: {0}")]
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
ISE_MSG,
|
||||||
database::{self},
|
database::{self},
|
||||||
users::{
|
users::{
|
||||||
auth::{AuthError, UserPasswordHashing},
|
auth::{AuthError, UserPasswordHashing},
|
||||||
@@ -34,16 +39,6 @@ pub enum UserError {
|
|||||||
#[error("Argon2 passhash error: {0}")]
|
#[error("Argon2 passhash error: {0}")]
|
||||||
PassHashError(argon2::password_hash::Error),
|
PassHashError(argon2::password_hash::Error),
|
||||||
}
|
}
|
||||||
impl From<rusqlite::Error> for UserError {
|
|
||||||
fn from(error: rusqlite::Error) -> Self {
|
|
||||||
UserError::DatabaseError(error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<argon2::password_hash::Error> for UserError {
|
|
||||||
fn from(err: argon2::password_hash::Error) -> Self {
|
|
||||||
UserError::PassHashError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn get_by_id(id: Uuid) -> Result<User, UserError> {
|
pub fn get_by_id(id: Uuid) -> Result<User, UserError> {
|
||||||
@@ -176,3 +171,32 @@ impl User {
|
|||||||
self.id == Uuid::nil()
|
self.id == Uuid::nil()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<rusqlite::Error> for UserError {
|
||||||
|
fn from(error: rusqlite::Error) -> Self {
|
||||||
|
UserError::DatabaseError(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<argon2::password_hash::Error> 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -5,7 +9,7 @@ use sha2::{Digest, Sha256};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database,
|
ISE_MSG, database,
|
||||||
users::{User, auth},
|
users::{User, auth},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +48,19 @@ impl From<rusqlite::Error> for SessionError {
|
|||||||
SessionError::DatabaseError(error.to_string())
|
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 {
|
impl Session {
|
||||||
pub fn get_by_id(id: Uuid) -> Result<Session, SessionError> {
|
pub fn get_by_id(id: Uuid) -> Result<Session, SessionError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user