diff --git a/Cargo.lock b/Cargo.lock index 8123a0d..1e373af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,6 +565,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "time 0.1.45", "wasm-bindgen", "winapi", @@ -1989,6 +1990,7 @@ dependencies = [ "jsonwebtoken", "log", "serde", + "serde_json", "sqlx", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index a52f675..b687879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] actix-web = "4.3.1" argon2 = "0.5.1" -chrono = "0.4.26" +chrono = { version = "0.4.26", features = ["serde"] } config = "0.13.3" derive_more = "0.99.17" env_logger = "0.10.0" @@ -17,6 +17,7 @@ handlebars = "4.3.7" jsonwebtoken = "8.3.0" log = "0.4.19" serde = { version = "1.0.177", features = ["derive"] } +serde_json = "1.0.104" sqlx = { version = "0.7.1", features = ["runtime-async-std-native-tls", "postgres", "chrono", "uuid"] } tokio = { version = "1.30.0", features = ["full"] } diff --git a/src/auth/extractors.rs b/src/auth/extractors.rs new file mode 100644 index 0000000..d76fb8f --- /dev/null +++ b/src/auth/extractors.rs @@ -0,0 +1,44 @@ +use std::{ops::Deref, pin::Pin}; + +use actix_web::{FromRequest, HttpMessage, error::ErrorUnauthorized}; +use futures_util::Future; + +use crate::models::dto_models::ResponseDTO; + +use super::models::AuthenticationInfo; + +pub struct Authenticated { + value: AuthenticationInfo, +} + +impl Authenticated { + pub fn new(auth_info: AuthenticationInfo) -> Self { + Authenticated { value: auth_info } + } +} + +impl Deref for Authenticated { + type Target = AuthenticationInfo; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl FromRequest for Authenticated { + type Error = actix_web::Error; + type Future = Pin> + Send>>; + + fn from_request(req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { + let auth_info = req.extensions().get::().cloned(); + + return Box::pin(async { + match auth_info { + Some(auth_info) => Ok(Authenticated::new(auth_info)), + None => { + Err(ErrorUnauthorized(ResponseDTO::new("Unauthorized").message("Authentication required for this resource"))) + } + } + }); + } +} diff --git a/src/auth/handlers.rs b/src/auth/handlers.rs index 0c74d61..1adfb91 100644 --- a/src/auth/handlers.rs +++ b/src/auth/handlers.rs @@ -1,19 +1,31 @@ use std::collections::HashMap; +use super::extractors::Authenticated; +use super::middlewares::require_auth_middleware::RequireAuthMiddlewareInitializer; use super::models::JsonTokenClaims; +use crate::auth::dto::*; use crate::auth::repositories::Repository; use crate::models::dto_models::ResponseDTO; +use crate::models::user_models::FilteredUser; use crate::states::db_state::DBState; use crate::{auth::utils::JwtUtils, models::user_models::User}; -use actix_web::{post, web, HttpResponse, Responder}; +use actix_web::{get, post, web, HttpResponse, Responder}; use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::SaltString; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use chrono::{Duration, Utc}; -use crate::auth::dto::*; pub fn auth_config(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("api/auth").service(login).service(register)); + cfg.service( + web::scope("api/auth") + .service(login) + .service(register) + .service( + web::scope("") + .wrap(RequireAuthMiddlewareInitializer) + .service(user_route), + ), + ); } #[post("login")] @@ -64,7 +76,10 @@ pub async fn login(data: web::Json, db_state: web::Data) -> i } //end function login #[post("register")] -pub async fn register(data: web::Json, db_state: web::Data) -> impl Responder { +pub async fn register( + data: web::Json, + db_state: web::Data, +) -> impl Responder { let RegisterDTO { email, password, @@ -97,4 +112,23 @@ pub async fn register(data: web::Json, db_state: web::Data ResponseDTO::new(HashMap::from([("id", user.id)])).message("User created successfully"), ), } -} +} //end function register + +#[get("user")] +pub async fn user_route(auth: Authenticated, db_state: web::Data) -> impl Responder { + let user: Option = Repository::::get_by_email(&db_state.pool, &auth.email).await; + + match user { + Some(user) => { + let filtered_user: Result = user.try_into(); + match filtered_user { + Ok(filtered_user) => HttpResponse::Ok() + .json(ResponseDTO::new(filtered_user).message("Authenticated user")), + Err(e) => HttpResponse::InternalServerError() + .json(ResponseDTO::new(e.to_string()).message("Unable to build user response")), + } + } + None => HttpResponse::NotFound() + .json(ResponseDTO::new("Not found").message("User record not found")), + } +} // end user function diff --git a/src/auth/middlewares/auth_middleware.rs b/src/auth/middlewares/auth_middleware.rs index 352e0fd..c057174 100644 --- a/src/auth/middlewares/auth_middleware.rs +++ b/src/auth/middlewares/auth_middleware.rs @@ -68,14 +68,8 @@ where .get(http::header::AUTHORIZATION) .map(|h| { let authorization_header = h.to_str().expect("Unable to convert to str"); - log::error!( - "Authorization header({}): {}", - authorization_header.len(), - authorization_header - ); if authorization_header.len() > 7 { let (_, token) = authorization_header.split_at(7); - log::error!("Authorization token: {}", token); token.to_string() } else { "".to_string() @@ -98,13 +92,11 @@ where .await; if let Ok(record) = record { - log::error!("Record: {:?}", record); let auth_info = AuthenticationInfo { id: record.id as u32, email: record.email, name: record.name, }; - log::error!("Auth info: {:?}", auth_info); req.request() .extensions_mut() .insert::(auth_info); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 3bb11e8..dee539a 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -3,4 +3,5 @@ pub mod models; pub mod handlers; pub mod utils; pub mod repositories; -pub mod middlewares; \ No newline at end of file +pub mod middlewares; +pub mod extractors; \ No newline at end of file diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 35bd23c..72e87b0 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,10 +1,9 @@ -use actix_web::{Responder, HttpResponse}; +use actix_web::{HttpResponse, Responder}; use crate::models::dto_models::ResponseDTO; pub mod healthcheck; pub async fn authenticated_route() -> impl Responder { - log::error!("Authenticated route"); return HttpResponse::Ok().json(ResponseDTO::new("This is the authenticated route")); -} \ No newline at end of file +} diff --git a/src/models/dto_models.rs b/src/models/dto_models.rs index 93746a3..9a17c4a 100644 --- a/src/models/dto_models.rs +++ b/src/models/dto_models.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use core::fmt; use std::ops::Deref; #[derive(Debug, Deserialize, Serialize)] @@ -7,6 +8,12 @@ pub struct ResponseDTO { data: T, } +impl fmt::Display for ResponseDTO { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", serde_json::to_string(&self).unwrap()) + } +} + impl ResponseDTO { pub fn new(data: T) -> Self { return ResponseDTO { diff --git a/src/models/user_models.rs b/src/models/user_models.rs index 0b65da0..172dacf 100644 --- a/src/models/user_models.rs +++ b/src/models/user_models.rs @@ -10,7 +10,7 @@ pub struct User { pub updated_at: DateTime, } -#[derive(Debug)] +#[derive(Debug, serde::Serialize)] pub struct FilteredUser { pub id: u32, pub name: String, @@ -18,3 +18,11 @@ pub struct FilteredUser { pub created_at: DateTime, pub updated_at: DateTime, } + +impl TryFrom for FilteredUser { + type Error = &'static str; + + fn try_from(user: User) -> Result { + Ok(FilteredUser { id: user.id, name: user.name, email: user.email, created_at: user.created_at, updated_at: user.updated_at }) + } +} \ No newline at end of file