diff --git a/Cargo.lock b/Cargo.lock index 8f5a8c3..c2b8926 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 8ce499d..d5f6eb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e81a994 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,4 @@ +#[derive(Debug, Clone)] +pub struct AppConfig { + pub jwt_token: String, +} diff --git a/src/endpoints/auth/login.rs b/src/endpoints/auth/login.rs index c155212..bc57427 100644 --- a/src/endpoints/auth/login.rs +++ b/src/endpoints/auth/login.rs @@ -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, state: Data) -> impl Responder { +pub async fn handler( + body: Json, + state: Data, +) -> Result { 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 })) } diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 0eb028e..8d7cde9 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -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()); +} diff --git a/src/endpoints/prelude.rs b/src/endpoints/prelude.rs index c5ea2e9..48c121f 100644 --- a/src/endpoints/prelude.rs +++ b/src/endpoints/prelude.rs @@ -1,4 +1,4 @@ pub use actix_web::{ - HttpResponse, Responder, get, post, + Responder, get, post, web::{Data, Json}, }; diff --git a/src/endpoints/users/list.rs b/src/endpoints/users/list.rs index 82e785c..b261a1e 100644 --- a/src/endpoints/users/list.rs +++ b/src/endpoints/users/list.rs @@ -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) -> impl Responder { - let users = User::find().all(&state.db).await.expect("database error"); +pub async fn handler(state: Data) -> Result { + 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))) } diff --git a/src/main.rs b/src/main.rs index 0837577..00a29a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) diff --git a/src/models/error.rs b/src/models/error.rs deleted file mode 100644 index fe1fecd..0000000 --- a/src/models/error.rs +++ /dev/null @@ -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), - } - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs index 0616f91..22d12a3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1 @@ -pub mod error; pub mod user; diff --git a/src/state.rs b/src/state.rs index 9b3681e..2dfaba8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,9 @@ use sea_orm::DatabaseConnection; +use crate::config::AppConfig; + #[derive(Debug, Clone)] pub struct AppState { pub db: DatabaseConnection, + pub config: AppConfig, } diff --git a/src/utils/errors.rs b/src/utils/errors.rs new file mode 100644 index 0000000..3ce055f --- /dev/null +++ b/src/utils/errors.rs @@ -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 { + HttpResponse::build(self.status_code()).json(self) + } +} diff --git a/src/utils/hasher.rs b/src/utils/hasher.rs index a3f644c..dccb8c6 100644 --- a/src/utils/hasher.rs +++ b/src/utils/hasher.rs @@ -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 { - 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 { + bcrypt::hash(password, BCRYPT_PASSWORD_COST).ok() +} + +pub fn verify(password: &str, hash: &str) -> bool { + bcrypt::verify(password, hash).is_ok_and(|v| v) } diff --git a/src/utils/jwt.rs b/src/utils/jwt.rs new file mode 100644 index 0000000..152851e --- /dev/null +++ b/src/utils/jwt.rs @@ -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 { + let key: Hmac = + 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())) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d04c8b7..d33e1ae 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,3 @@ -mod hasher; - -pub use hasher::PasswordHasher; +pub mod errors; +pub mod hasher; +pub mod jwt;