feat: authentication system
This commit is contained in:
parent
d8c29e1ec8
commit
37153c6e36
15 changed files with 852 additions and 18 deletions
144
src/routes/auth.rs
Normal file
144
src/routes/auth.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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"})))
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod auth;
|
||||
pub mod book;
|
||||
pub mod book_instance;
|
||||
pub mod owner;
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
|
|
|
|||
Reference in a new issue