More error handling improvements
This commit is contained in:
parent
eba30d1e9d
commit
75afab933d
@ -9,6 +9,7 @@ services:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
72
src/auth.rs
72
src/auth.rs
@ -2,8 +2,7 @@ use std::{array::TryFromSliceError, sync::LazyLock};
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{FromRef, FromRequestParts},
|
extract::{FromRef, FromRequestParts},
|
||||||
http::{request::Parts, StatusCode},
|
http::request::Parts,
|
||||||
response::IntoResponse,
|
|
||||||
RequestPartsExt,
|
RequestPartsExt,
|
||||||
};
|
};
|
||||||
use axum_extra::{
|
use axum_extra::{
|
||||||
@ -16,7 +15,7 @@ use rand::{rngs::OsRng, RngCore};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
use crate::{db, Pool};
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub const HASH_LENGTH: usize = 64;
|
pub const HASH_LENGTH: usize = 64;
|
||||||
pub const SALT_LENGTH: usize = 64;
|
pub const SALT_LENGTH: usize = 64;
|
||||||
@ -56,7 +55,7 @@ fn hash_scrypt(bytes: &[u8], salt: &[u8]) -> [u8; HASH_LENGTH] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verifieble scrypt hashed bytes
|
/// Verifieble scrypt hashed bytes
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))] // == OPERATOR MUSTN'T BE USED OUTSIZE OF TESTS
|
||||||
pub struct HashedBytes {
|
pub struct HashedBytes {
|
||||||
pub hash: [u8; HASH_LENGTH],
|
pub hash: [u8; HASH_LENGTH],
|
||||||
pub salt: [u8; SALT_LENGTH],
|
pub salt: [u8; SALT_LENGTH],
|
||||||
@ -122,6 +121,8 @@ pub struct Claims {
|
|||||||
pub exp: i64,
|
pub exp: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const JWT_ALGORITHM: jsonwebtoken::Algorithm = jsonwebtoken::Algorithm::HS256;
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
pub fn new(user_id: i32) -> Self {
|
pub fn new(user_id: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -130,13 +131,9 @@ impl Claims {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode(self) -> Result<Token, Error> {
|
pub fn encode(self) -> Result<Token, GeneralError> {
|
||||||
let access_token = encode(
|
let access_token = encode(&Header::new(JWT_ALGORITHM), &self, &KEYS.encoding_key)
|
||||||
&Header::new(jsonwebtoken::Algorithm::HS256),
|
.handle_internal("Token creation error")?;
|
||||||
&self,
|
|
||||||
&KEYS.encoding_key,
|
|
||||||
)
|
|
||||||
.map_err(|_| Error::TokenCreation)?;
|
|
||||||
let token = Token {
|
let token = Token {
|
||||||
access_token,
|
access_token,
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
@ -145,51 +142,40 @@ impl Claims {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
WrongCredentials,
|
|
||||||
TokenCreation,
|
|
||||||
Validation,
|
|
||||||
InvalidToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
let (status, error_message) = match self {
|
|
||||||
Error::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
|
|
||||||
Error::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
|
|
||||||
Error::Validation => (StatusCode::INTERNAL_SERVER_ERROR, "Token validation error"),
|
|
||||||
Error::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
|
|
||||||
};
|
|
||||||
(status, error_message).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[axum::async_trait]
|
#[axum::async_trait]
|
||||||
impl<T> FromRequestParts<T> for Claims
|
impl<T> FromRequestParts<T> for Claims
|
||||||
where
|
where
|
||||||
Pool: FromRef<T>,
|
Pool: FromRef<T>,
|
||||||
T: Sync,
|
T: Sync,
|
||||||
{
|
{
|
||||||
type Rejection = Error;
|
type Rejection = GeneralError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &T) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &T) -> Result<Self, Self::Rejection> {
|
||||||
|
const INVALID_TOKEN: GeneralError =
|
||||||
|
GeneralError::const_message(StatusCode::UNAUTHORIZED, "Invalid token");
|
||||||
|
|
||||||
let pool = Pool::from_ref(state);
|
let pool = Pool::from_ref(state);
|
||||||
let TypedHeader(Authorization(bearer)) = parts
|
let TypedHeader(Authorization(bearer)) = parts
|
||||||
.extract::<TypedHeader<Authorization<Bearer>>>()
|
.extract::<TypedHeader<Authorization<Bearer>>>()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::InvalidToken)?;
|
.map_err(|_| INVALID_TOKEN)?;
|
||||||
let claims: Claims = decode(bearer.token(), &KEYS.decoding_key, &Validation::default())
|
|
||||||
.map_err(|_| Error::InvalidToken)?
|
let claims: Claims = decode(
|
||||||
|
bearer.token(),
|
||||||
|
&KEYS.decoding_key,
|
||||||
|
&Validation::new(JWT_ALGORITHM),
|
||||||
|
)
|
||||||
|
.map_err(|_| INVALID_TOKEN)?
|
||||||
.claims;
|
.claims;
|
||||||
match db::users::exists(claims.user_id, &pool).await {
|
|
||||||
Ok(true) => Ok(claims),
|
db::users::exists(claims.user_id, &pool)
|
||||||
Ok(false) => Err(Error::WrongCredentials),
|
.await
|
||||||
Err(err) => {
|
.handle_internal("Token validation error")?
|
||||||
tracing::error!(%err);
|
.then_some(claims)
|
||||||
Err(Error::Validation)
|
.ok_or(GeneralError::const_message(
|
||||||
}
|
StatusCode::UNAUTHORIZED,
|
||||||
}
|
"Wrong credentials",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,12 +38,7 @@ impl PermissionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn can_read_guard(self) -> GeneralResult<()> {
|
fn can_read_guard(self) -> GeneralResult<()> {
|
||||||
if !self.can_read() {
|
self.can_read().then_some(()).item_not_found()?;
|
||||||
return Err(GeneralError::message(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"Item not found",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ pub async fn download(
|
|||||||
let mut name = db::file::get_name(params.file_id, &state.pool)
|
let mut name = db::file::get_name(params.file_id, &state.pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error getting file info")?
|
.handle_internal("Error getting file info")?
|
||||||
.ok_or_else(GeneralError::item_not_found)?;
|
.item_not_found()?;
|
||||||
name = name
|
name = name
|
||||||
.chars()
|
.chars()
|
||||||
.fold(String::with_capacity(name.len()), |mut result, char| {
|
.fold(String::with_capacity(name.len()), |mut result, char| {
|
||||||
|
@ -36,7 +36,7 @@ pub async fn modify(
|
|||||||
.write(params.file_id)
|
.write(params.file_id)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error writing to the file")?
|
.handle_internal("Error writing to the file")?
|
||||||
.ok_or_else(GeneralError::item_not_found)?;
|
.item_not_found()?;
|
||||||
|
|
||||||
let (hash, size) = crate::FileStorage::write_to_file(&mut file, &mut field)
|
let (hash, size) = crate::FileStorage::write_to_file(&mut file, &mut field)
|
||||||
.await
|
.await
|
||||||
|
@ -29,12 +29,12 @@ pub async fn structure(
|
|||||||
let folder_id = db::folder::process_id(params.folder_id, claims.user_id, &pool)
|
let folder_id = db::folder::process_id(params.folder_id, claims.user_id, &pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error processing id")?
|
.handle_internal("Error processing id")?
|
||||||
.ok_or_else(GeneralError::item_not_found)?;
|
.item_not_found()?;
|
||||||
|
|
||||||
let folder = db::folder::get_by_id(folder_id, &pool)
|
let folder = db::folder::get_by_id(folder_id, &pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error getting folder info")?
|
.handle_internal("Error getting folder info")?
|
||||||
.ok_or_else(GeneralError::item_not_found)?;
|
.item_not_found()?;
|
||||||
|
|
||||||
let mut response: FolderStructure = folder.into();
|
let mut response: FolderStructure = folder.into();
|
||||||
let mut stack = vec![&mut response];
|
let mut stack = vec![&mut response];
|
||||||
|
@ -29,7 +29,7 @@ pub async fn set(
|
|||||||
let folder_info = db::folder::get_by_id(params.folder_id, &pool)
|
let folder_info = db::folder::get_by_id(params.folder_id, &pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error getting folder info")?
|
.handle_internal("Error getting folder info")?
|
||||||
.ok_or_else(GeneralError::item_not_found)?;
|
.item_not_found()?;
|
||||||
if folder_info.owner_id == params.user_id {
|
if folder_info.owner_id == params.user_id {
|
||||||
return Err(GeneralError::message(
|
return Err(GeneralError::message(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use axum::Form;
|
use axum::Form;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{authenticate_user, Error, Token},
|
auth::{authenticate_user, Token},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -14,10 +14,13 @@ pub struct Params {
|
|||||||
pub async fn login(
|
pub async fn login(
|
||||||
State(pool): State<Pool>,
|
State(pool): State<Pool>,
|
||||||
Form(payload): Form<Params>,
|
Form(payload): Form<Params>,
|
||||||
) -> Result<Json<Token>, Error> {
|
) -> GeneralResult<Json<Token>> {
|
||||||
let user_id = authenticate_user(&payload.username, &payload.password, &pool)
|
let user_id = authenticate_user(&payload.username, &payload.password, &pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::WrongCredentials)?
|
.handle_internal("Error getting user from database")?
|
||||||
.ok_or(Error::WrongCredentials)?;
|
.handle(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"User with this name and password doesn't exist",
|
||||||
|
)?;
|
||||||
Claims::new(user_id).encode().map(Json)
|
Claims::new(user_id).encode().map(Json)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ pub async fn put(
|
|||||||
claims: Claims,
|
claims: Claims,
|
||||||
Json(params): Json<Params>,
|
Json(params): Json<Params>,
|
||||||
) -> GeneralResult<Json<db::users::UserInfo>> {
|
) -> GeneralResult<Json<db::users::UserInfo>> {
|
||||||
params.validate().map_err(GeneralError::validation)?;
|
params.validate().handle_validation()?;
|
||||||
db::users::update(claims.user_id, ¶ms.username, ¶ms.email, &pool)
|
db::users::update(claims.user_id, ¶ms.username, ¶ms.email, &pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error updating the user")
|
.handle_internal("Error updating the user")
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use axum::Form;
|
use axum::Form;
|
||||||
use axum_extra::either::Either;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use validator::{Validate, ValidationError};
|
use validator::{Validate, ValidationError};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{Error, HashedBytes, Token},
|
auth::{HashedBytes, Token},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,23 +47,17 @@ fn validate_password(password: &str) -> Result<(), ValidationError> {
|
|||||||
pub async fn register(
|
pub async fn register(
|
||||||
State(pool): State<Pool>,
|
State(pool): State<Pool>,
|
||||||
Form(params): Form<Params>,
|
Form(params): Form<Params>,
|
||||||
) -> Result<Json<Token>, Either<GeneralError, Error>> {
|
) -> GeneralResult<Json<Token>> {
|
||||||
params
|
params.validate().handle_validation()?;
|
||||||
.validate()
|
|
||||||
.map_err(GeneralError::validation)
|
|
||||||
.map_err(Either::E1)?;
|
|
||||||
|
|
||||||
let password = HashedBytes::hash_bytes(params.password.as_bytes()).as_bytes();
|
let password = HashedBytes::hash_bytes(params.password.as_bytes()).as_bytes();
|
||||||
let id = db::users::create_user(¶ms.username, ¶ms.email, &password, &pool)
|
let id = db::users::create_user(¶ms.username, ¶ms.email, &password, &pool)
|
||||||
.await
|
.await
|
||||||
.handle_internal("Error creating the user")
|
.handle_internal("Error creating the user")?
|
||||||
.map_err(Either::E1)?
|
|
||||||
.handle(
|
.handle(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"The username or the email are taken",
|
"The username or the email are taken",
|
||||||
)
|
)?;
|
||||||
.map_err(Either::E1)?;
|
|
||||||
|
|
||||||
let token = Claims::new(id).encode().map_err(Either::E2)?;
|
Claims::new(id).encode().map(Json)
|
||||||
Ok(Json(token))
|
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,16 @@ use axum::{http::StatusCode, response::IntoResponse};
|
|||||||
|
|
||||||
type BoxError = Box<dyn std::error::Error>;
|
type BoxError = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
|
/// Common error type for the project
|
||||||
pub struct GeneralError {
|
pub struct GeneralError {
|
||||||
|
/// Response status code
|
||||||
pub status_code: StatusCode,
|
pub status_code: StatusCode,
|
||||||
|
/// Message to send to the user
|
||||||
pub message: Cow<'static, str>,
|
pub message: Cow<'static, str>,
|
||||||
|
/// Error to log
|
||||||
pub error: Option<BoxError>,
|
pub error: Option<BoxError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type GeneralResult<T> = Result<T, GeneralError>;
|
|
||||||
|
|
||||||
impl GeneralError {
|
impl GeneralError {
|
||||||
pub fn message(status_code: StatusCode, message: impl Into<Cow<'static, str>>) -> Self {
|
pub fn message(status_code: StatusCode, message: impl Into<Cow<'static, str>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -21,15 +23,10 @@ impl GeneralError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
pub const fn const_message(status_code: StatusCode, message: &'static str) -> Self {
|
||||||
pub fn validation(error: validator::ValidationErrors) -> Self {
|
Self {
|
||||||
Self::message(StatusCode::BAD_REQUEST, error.to_string())
|
status_code,
|
||||||
}
|
message: Cow::Borrowed(message),
|
||||||
|
|
||||||
pub const fn item_not_found() -> Self {
|
|
||||||
GeneralError {
|
|
||||||
status_code: StatusCode::NOT_FOUND,
|
|
||||||
message: Cow::Borrowed("Item not found"),
|
|
||||||
error: None,
|
error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,6 +41,8 @@ impl IntoResponse for GeneralError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type GeneralResult<T> = Result<T, GeneralError>;
|
||||||
|
|
||||||
pub trait ErrorHandlingExt<T, E>
|
pub trait ErrorHandlingExt<T, E>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
@ -86,3 +85,25 @@ impl<T> ErrorHandlingExt<T, Infallible> for Option<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ItemNotFoundExt<T> {
|
||||||
|
fn item_not_found(self) -> Result<T, GeneralError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ItemNotFoundExt<T> for Option<T> {
|
||||||
|
fn item_not_found(self) -> GeneralResult<T> {
|
||||||
|
const ITEM_NOT_FOUND_ERROR: GeneralError =
|
||||||
|
GeneralError::const_message(StatusCode::NOT_FOUND, "Item not found");
|
||||||
|
self.ok_or(ITEM_NOT_FOUND_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ValidationExt<T> {
|
||||||
|
fn handle_validation(self) -> GeneralResult<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ValidationExt<T> for Result<T, validator::ValidationErrors> {
|
||||||
|
fn handle_validation(self) -> GeneralResult<T> {
|
||||||
|
self.map_err(|err| GeneralError::message(StatusCode::BAD_REQUEST, err.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
auth::Claims,
|
auth::Claims,
|
||||||
db::{self, permissions::PermissionExt as _},
|
db::{self, permissions::PermissionExt as _},
|
||||||
errors::{ErrorHandlingExt as _, GeneralError, GeneralResult},
|
errors::{
|
||||||
|
ErrorHandlingExt as _, GeneralError, GeneralResult, ItemNotFoundExt as _,
|
||||||
|
ValidationExt as _,
|
||||||
|
},
|
||||||
AppState, Pool,
|
AppState, Pool,
|
||||||
};
|
};
|
||||||
pub use axum::{
|
pub use axum::{
|
||||||
|
Reference in New Issue
Block a user