feat: authentication system

This commit is contained in:
Ninjdai 2025-08-03 01:50:18 +02:00
parent d8c29e1ec8
commit 37153c6e36
15 changed files with 852 additions and 18 deletions

144
src/routes/auth.rs Normal file
View file

@ -0,0 +1,144 @@
use std::sync::{Arc, LazyLock};
use argon2::PasswordHash;
use axum::{
extract::{FromRequestParts, Request, State}, http::{request::Parts, HeaderMap, StatusCode}, middleware::Next, response::{IntoResponse, Response}, routing::{get, post}, Json, RequestPartsExt, Router
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{entities::user, AppState, KEYS};
pub async fn auth_middleware(
_claims: Claims,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let response = next.run(request).await;
Ok(response)
}
#[axum::debug_handler]
#[utoipa::path(
post,
path = "/auth",
request_body = AuthPayload,
responses(
(status = OK, body = AuthBody, description = "Successfully authenticated"),
(status = UNAUTHORIZED, description = "Wrong credentials"),
(status = BAD_REQUEST, description = "Missing credentials"),
(status = INTERNAL_SERVER_ERROR, description = "Token creation error"),
(status = BAD_REQUEST, description = "Invalid token")
),
summary = "Authenticate to access the API",
tag = "auth-api",
)]
pub async fn auth(State(state): State<Arc<AppState>>, Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
log::debug!("Payload: {payload:?}");
if payload.username.is_empty() || payload.password.is_empty() {
return Err(AuthError::MissingCredentials);
}
match user::Entity::find().filter(user::Column::Username.eq(payload.username)).one(state.db_conn.as_ref()).await {
Err(_) | Ok(None) => return Err(AuthError::WrongCredentials),
Ok(Some(user)) => {
user.verify_password(payload.password);
let claims = Claims {
sub: user.username,
exp: 2000000000,
};
let token = encode(&Header::default(), &claims, &KEYS.encoding)
.map_err(|_| AuthError::TokenCreation)?;
Ok(Json(AuthBody::new(token)))
}
}
}
impl AuthBody {
fn new(access_token: String) -> Self {
Self {
access_token,
token_type: "Bearer".to_string(),
}
}
}
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _s: &S) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AuthError::InvalidToken)?;
let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
.map_err(|_| AuthError::InvalidToken)?;
Ok(token_data.claims)
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
pub struct Keys {
encoding: EncodingKey,
decoding: DecodingKey,
}
impl Keys {
pub fn new(secret: &[u8]) -> Self {
Self {
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
sub: String,
exp: usize,
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct AuthBody {
access_token: String,
token_type: String,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct AuthPayload {
username: String,
password: String,
}
#[derive(Debug)]
pub enum AuthError {
WrongCredentials,
MissingCredentials,
TokenCreation,
InvalidToken,
}

View file

@ -20,6 +20,7 @@ struct BookByIdParams(u32);
get,
path = "/book/id/{id}",
params(BookByIdParams),
security(("jwt" = [])),
responses(
(status = OK, body = book::Model, description = "Found book with corresponding ID in the database", examples(
("Found regular book" = (value = json!({"author": "Pierre Bottero", "ean": "9782700234015", "id": 5642, "title": "Ellana l'envol"}))),
@ -52,6 +53,7 @@ struct BookByEanParams(String);
get,
path = "/book/ean/{ean}",
params(BookByEanParams),
security(("jwt" = [])),
responses(
(status = OK, body = book::Model, description = "Found book with corresponding EAN", examples(
("Found regular book" = (value = json!({"author": "Pierre Bottero", "ean": "9782700234015", "id": 5642, "title": "Ellana l'envol"})))

View file

@ -19,6 +19,7 @@ struct BookInstanceByIdParams(u32);
get,
path = "/book_instance/{id}",
params(BookInstanceByIdParams),
security(("jwt" = [])),
responses(
(status = OK, body = book_instance::Model, description = "Found book instance with corresponding ID in the database"),
(status = NOT_FOUND, description = "No book instance with this id exists in the database")
@ -50,6 +51,7 @@ pub struct BookInstanceCreateParams {
post,
path = "/book_instance",
request_body = BookInstanceCreateParams,
security(("jwt" = [])),
responses(
(status = OK, body = book_instance::Model, description = "Successfully created book instance"),
),
@ -93,6 +95,7 @@ pub struct BookInstanceUpdateParams {
path = "/book_instance/{id}",
params(BookInstanceByIdParams),
request_body = BookInstanceUpdateParams,
security(("jwt" = [])),
responses(
(status = OK, body = book_instance::Model, description = "Successfully updated book instance"),
(status = NOT_FOUND, description = "No book instance with specified id was found"),
@ -148,6 +151,7 @@ pub struct BookInstanceSaleParams {
path = "/book_instance/{id}/sell",
params(BookInstanceByIdParams),
request_body = BookInstanceSaleParams,
security(("jwt" = [])),
responses(
(status = OK, body = book_instance::Model, description = "Successfully sold book instance"),
(status = NOT_FOUND, description = "No book instance with specified id was found"),
@ -186,6 +190,7 @@ pub async fn sell_book_instance(
post,
path = "/book_instance/bulk",
request_body = Vec<BookInstanceCreateParams>,
security(("jwt" = [])),
responses(
(status = OK, description = "Successfully created book instances"),
),

View file

@ -1,3 +1,4 @@
pub mod auth;
pub mod book;
pub mod book_instance;
pub mod owner;

View file

@ -19,6 +19,7 @@ struct OwnerByIdParams(u32);
get,
path = "/owner/{id}",
params(OwnerByIdParams),
security(("jwt" = [])),
responses(
(status = OK, body = owner::Model, description = "Found owner with corresponding ID in the database"),
(status = NOT_FOUND, description = "No owner with this id exists in the database")
@ -50,6 +51,7 @@ pub struct OwnerCreateParams {
post,
path = "/owner",
request_body = OwnerCreateParams,
security(("jwt" = [])),
responses(
(status = OK, body = owner::Model, description = "Successfully created owner"),
),
@ -96,6 +98,7 @@ pub struct OwnerUpdateParams {
path = "/owner/{id}",
params(OwnerByIdParams),
request_body = OwnerUpdateParams,
security(("jwt" = [])),
responses(
(status = OK, body = owner::Model, description = "Successfully updated owner"),
(status = NOT_FOUND, description = "No owner with this id exists in the database")
@ -145,6 +148,7 @@ pub async fn update_owner(
#[utoipa::path(
get,
path = "/owners",
security(("jwt" = [])),
responses(
(status = OK, body = Vec<owner::Model>, description = "List of owners"),
),

View file

@ -16,6 +16,7 @@ use futures_util::{sink::SinkExt, stream::StreamExt};
#[utoipa::path(
get,
path = "/ws",
security(("jwt" = [])),
responses(
(status = SWITCHING_PROTOCOLS, description = "Succesfully reached the websocket, now upgrade to establish the connection"),
),