improve error handling, cleanup, jwt

This commit is contained in:
Mia Pilchová 2025-02-24 13:22:49 +01:00
parent 618d1c40d6
commit 443bfa29eb
15 changed files with 154 additions and 58 deletions

52
Cargo.lock generated
View File

@ -35,7 +35,7 @@ dependencies = [
"brotli",
"bytes",
"bytestring",
"derive_more",
"derive_more 0.99.19",
"encoding_rs",
"flate2",
"futures-core",
@ -151,7 +151,7 @@ dependencies = [
"bytestring",
"cfg-if",
"cookie",
"derive_more",
"derive_more 0.99.19",
"encoding_rs",
"futures-core",
"futures-util",
@ -528,10 +528,13 @@ version = "0.1.0"
dependencies = [
"actix-web",
"bcrypt",
"derive_more 2.0.1",
"dotenvy",
"hmac",
"jwt",
"sea-orm",
"serde",
"sha2",
]
[[package]]
@ -601,6 +604,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.16.2"
@ -713,13 +725,35 @@ version = "0.99.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f"
dependencies = [
"convert_case",
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.98",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case 0.7.1",
"proc-macro2",
"quote",
"syn 2.0.98",
"unicode-xid",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -2826,6 +2860,18 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "url"
version = "2.5.4"

View File

@ -14,3 +14,6 @@ dotenvy = "0.15"
serde = { version = "1.0", features = ["derive"] }
bcrypt = "0.17"
jwt = "0.16"
hmac = "0.12"
sha2 = "0.10"
derive_more = { version = "2.0", features = ["full"] }

4
src/config.rs Normal file
View File

@ -0,0 +1,4 @@
#[derive(Debug, Clone)]
pub struct AppConfig {
pub jwt_token: String,
}

View File

@ -1,12 +1,11 @@
use crate::{
endpoints::prelude::*,
entities::{prelude::User, user},
models::{error::ErrorResponse, user::UserModel},
state::AppState,
utils::PasswordHasher,
utils::{errors::GenericError, hasher, jwt},
};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct LoginRequest {
@ -14,23 +13,32 @@ struct LoginRequest {
password: String,
}
#[derive(Serialize)]
struct LoginResponse {
token: String,
}
#[post("/login")]
pub async fn handler(body: Json<LoginRequest>, state: Data<AppState>) -> impl Responder {
pub async fn handler(
body: Json<LoginRequest>,
state: Data<AppState>,
) -> Result<impl Responder, GenericError> {
let user = User::find()
.filter(user::Column::Email.eq(body.email.clone()))
.one(&state.db)
.await
.expect("database error");
.map_err(|e| GenericError::new(e.to_string().as_str()))?;
if user.is_none() {
return HttpResponse::Unauthorized().json(ErrorResponse::new(401, "invalid email"));
if user.is_none() || !hasher::verify(&body.password, &user.as_ref().unwrap().password) {
return Err(GenericError::new("invalid credentials"));
}
let user = user.unwrap();
if !PasswordHasher::verify(&body.password, &user.password) {
return HttpResponse::Unauthorized().json(ErrorResponse::new(401, "invalid password"));
}
let token = jwt::sign(
user.id.to_string().as_str(),
state.config.jwt_token.as_str(),
)?;
HttpResponse::Ok().json(UserModel::from(user))
Ok(Json(LoginResponse { token }))
}

View File

@ -1,4 +1,12 @@
use actix_web::web::ServiceConfig;
pub mod auth;
pub mod misc;
mod prelude;
pub mod users;
pub fn register(cfg: &mut ServiceConfig) {
cfg.service(auth::scope());
cfg.service(misc::scope());
cfg.service(users::scope());
}

View File

@ -1,4 +1,4 @@
pub use actix_web::{
HttpResponse, Responder, get, post,
Responder, get, post,
web::{Data, Json},
};

View File

@ -2,11 +2,15 @@ use sea_orm::EntityTrait;
use crate::{
endpoints::prelude::*, entities::prelude::User, models::user::UserModel, state::AppState,
utils::errors::GenericError,
};
#[get("/")]
pub async fn handler(state: Data<AppState>) -> impl Responder {
let users = User::find().all(&state.db).await.expect("database error");
pub async fn handler(state: Data<AppState>) -> Result<impl Responder, GenericError> {
let users = User::find()
.all(&state.db)
.await
.map_err(|e| GenericError::new(e.to_string().as_str()))?;
HttpResponse::Ok().json(UserModel::from_many(users))
Ok(Json(UserModel::from_many(users)))
}

View File

@ -1,3 +1,4 @@
pub mod config;
pub mod endpoints;
pub mod entities;
pub mod models;
@ -7,6 +8,7 @@ pub mod utils;
use std::env;
use actix_web::{App, HttpServer, web};
use config::AppConfig;
use sea_orm::Database;
use state::AppState;
@ -29,18 +31,18 @@ async fn main() -> Result<(), std::io::Error> {
// TODO: migrations
let state = AppState { db };
let config = AppConfig {
jwt_token: env::var("JWT_SECRET").expect("JWT_SECRET is required!"),
};
let state = AppState { db, config };
println!("starting a server at {}:{}", host, port);
let app_factory = move || {
App::new()
.app_data(web::Data::new(state.clone()))
.configure(|cfg| {
cfg.service(endpoints::auth::scope());
cfg.service(endpoints::misc::scope());
cfg.service(endpoints::users::scope());
})
.configure(|cfg| endpoints::register(cfg))
};
HttpServer::new(app_factory)

View File

@ -1,20 +0,0 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct ErrorResponse {
pub status_code: i16,
pub message: String,
}
impl ErrorResponse {
pub fn server_error(message: &str) -> Self {
Self::new(500, message)
}
pub fn new(status_code: i16, message: &str) -> Self {
Self {
status_code,
message: String::from(message),
}
}
}

View File

@ -1,2 +1 @@
pub mod error;
pub mod user;

View File

@ -1,6 +1,9 @@
use sea_orm::DatabaseConnection;
use crate::config::AppConfig;
#[derive(Debug, Clone)]
pub struct AppState {
pub db: DatabaseConnection,
pub config: AppConfig,
}

23
src/utils/errors.rs Normal file
View File

@ -0,0 +1,23 @@
use actix_web::{HttpResponse, error};
use derive_more::derive::{Display, Error};
use serde::Serialize;
#[derive(Debug, Display, Serialize, Error)]
#[display("GenericError: {error}")]
pub struct GenericError {
pub error: String,
}
impl GenericError {
pub fn new(message: &str) -> Self {
GenericError {
error: String::from(message),
}
}
}
impl error::ResponseError for GenericError {
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
HttpResponse::build(self.status_code()).json(self)
}
}

View File

@ -1,14 +1,12 @@
pub struct PasswordHasher {}
impl PasswordHasher {
// DO NOT CHANGE THIS VALUE OR HELL WILL RISE
pub const BCRYPT_PASSWORD_COST: u32 = 13;
// DO NOT CHANGE THIS VALUE OR HELL WILL RISE
pub const BCRYPT_PASSWORD_COST: u32 = 13;
pub fn hash(password: &str) -> Option<String> {
bcrypt::hash(password, Self::BCRYPT_PASSWORD_COST).ok()
}
pub fn verify(password: &str, hash: &str) -> bool {
bcrypt::verify(password, hash).is_ok_and(|v| v)
}
pub fn hash(password: &str) -> Option<String> {
bcrypt::hash(password, BCRYPT_PASSWORD_COST).ok()
}
pub fn verify(password: &str, hash: &str) -> bool {
bcrypt::verify(password, hash).is_ok_and(|v| v)
}

18
src/utils/jwt.rs Normal file
View File

@ -0,0 +1,18 @@
use hmac::{Hmac, Mac};
use jwt::SignWithKey;
use sha2::Sha384;
use std::collections::BTreeMap;
use super::errors::GenericError;
pub fn sign(user_id: &str, jwt_secret: &str) -> Result<String, GenericError> {
let key: Hmac<Sha384> =
Hmac::new_from_slice(jwt_secret.as_bytes()).map_err(|e| GenericError::new(&e.to_string()))?;
let mut claims = BTreeMap::new();
claims.insert("sub", user_id);
claims
.sign_with_key(&key)
.map_err(|e| GenericError::new(&e.to_string()))
}

View File

@ -1,3 +1,3 @@
mod hasher;
pub use hasher::PasswordHasher;
pub mod errors;
pub mod hasher;
pub mod jwt;