From 21f1289b964120a78a65b6af944e3460c9f3503e Mon Sep 17 00:00:00 2001 From: Kent Tristan Yves Sarmiento Date: Thu, 21 Dec 2023 06:52:44 +0000 Subject: [PATCH 1/2] feat: user login --- Cargo.lock | 1 + README.md | 2 +- link-for-later/Cargo.toml | 1 + link-for-later/src/controller/users.rs | 12 +++++++--- link-for-later/src/service.rs | 4 +++- link-for-later/src/service/users.rs | 30 ++++++++++++++++++++----- link-for-later/src/state.rs | 7 ++++++ link-for-later/src/types.rs | 2 ++ link-for-later/src/types/auth.rs | 31 ++++++++++++++++++++++++++ link-for-later/src/types/errors.rs | 5 ++++- link-for-later/src/types/response.rs | 10 +++++++-- 11 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 link-for-later/src/types/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 6545522..bd06d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1300,6 +1300,7 @@ dependencies = [ "chrono", "futures", "http-body-util", + "jsonwebtoken", "mockall", "mongodb", "serde", diff --git a/README.md b/README.md index cfa310c..59025ac 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Link for Later Service provides an API to save links in your personal library fo ## Features -- User registration/login for personal library +- User registration/login for a personal library - Saving of links to library - Analysis of saved links in library (to add information such as label, category, description, estimated time) diff --git a/link-for-later/Cargo.toml b/link-for-later/Cargo.toml index 1b16139..1e3fd8d 100644 --- a/link-for-later/Cargo.toml +++ b/link-for-later/Cargo.toml @@ -12,6 +12,7 @@ bson = "2.8.1" chrono = { version = "0.4.31", default-features = false, features=["clock", "serde"] } futures = "0.3.29" http-body-util = "0.1.0" +jsonwebtoken = "9.2.0" mongodb = "2.8.0" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" diff --git a/link-for-later/src/controller/users.rs b/link-for-later/src/controller/users.rs index abe8960..37265e4 100644 --- a/link-for-later/src/controller/users.rs +++ b/link-for-later/src/controller/users.rs @@ -2,7 +2,7 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, routing, Js use crate::{ state::AppState, - types::{entity::UserInfo, LoginRequest, RegisterRequest}, + types::{entity::UserInfo, LoginRequest, LoginResponse, RegisterRequest}, }; const USERS_SIGNUP_ROUTE: &str = "/v1/users/register"; @@ -39,13 +39,19 @@ async fn login( Json(payload): Json, ) -> impl IntoResponse { let users_repo = app_state.users_repo().clone(); + let secret_key = app_state.secret_key(); let user_info: UserInfo = payload.into(); match app_state .users_service() - .login(Box::new(users_repo), &user_info) + .login(Box::new(users_repo), secret_key, &user_info) .await { - Ok(_) => StatusCode::OK.into_response(), + Ok(token) => { + let response = LoginResponse { + token: token.jwt().to_string(), + }; + (StatusCode::OK, Json(response)).into_response() + } Err(e) => { tracing::error!("Error: {}", e); e.into_response() diff --git a/link-for-later/src/service.rs b/link-for-later/src/service.rs index d0337e0..4daad7a 100644 --- a/link-for-later/src/service.rs +++ b/link-for-later/src/service.rs @@ -7,6 +7,7 @@ use mockall::{automock, predicate::*}; use crate::{ repository, types::{ + auth::Token, entity::{LinkItem, UserInfo}, Result, }, @@ -50,8 +51,9 @@ pub trait Users { async fn login( &self, users_repo: Box, + secret_key: &str, user_info: &UserInfo, - ) -> Result; + ) -> Result; } pub mod links; diff --git a/link-for-later/src/service/users.rs b/link-for-later/src/service/users.rs index daa08bc..62ea295 100644 --- a/link-for-later/src/service/users.rs +++ b/link-for-later/src/service/users.rs @@ -1,10 +1,15 @@ use axum::async_trait; use chrono::Utc; +use jsonwebtoken::{encode, EncodingKey, Header}; use crate::{ repository, service::Users as UsersService, - types::{entity::UserInfo, AppError, Result}, + types::{ + auth::{Claims, Token}, + entity::UserInfo, + AppError, Result, + }, }; pub struct ServiceProvider {} @@ -35,11 +40,26 @@ impl UsersService for ServiceProvider { async fn login( &self, users_repo: Box, + secret_key: &str, user_info: &UserInfo, - ) -> Result { - users_repo.find_by_user(&user_info.email).await?; + ) -> Result { + let retrieved_user_info = users_repo.find_by_user(&user_info.email).await?; + + if retrieved_user_info.password != user_info.password { + tracing::error!("Error: invalid password for user {}", &user_info.email); + return Err(AppError::InvalidPassword); + } + + let claims = Claims::new(&retrieved_user_info.email); + let Ok(token) = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret_key.as_ref()), + ) else { + tracing::error!("Error: cannot generate token"); + return Err(AppError::ServerError); + }; - // TODO: login process, return token - Ok(user_info.clone()) + Ok(Token::new(&token)) } } diff --git a/link-for-later/src/state.rs b/link-for-later/src/state.rs index 057302d..e540aba 100644 --- a/link-for-later/src/state.rs +++ b/link-for-later/src/state.rs @@ -10,6 +10,7 @@ pub struct AppState { users_service: DynUsersService, links_repo: DynLinksRepository, users_repo: DynUsersRepository, + secret_key: String, } impl AppState { @@ -19,11 +20,13 @@ impl AppState { links_repo: DynLinksRepository, users_repo: DynUsersRepository, ) -> Self { + let secret_key = std::env::var("MONGODB_URI").map_or_else(|_| String::new(), |key| key); Self { links_service, users_service, links_repo, users_repo, + secret_key, } } @@ -42,4 +45,8 @@ impl AppState { pub fn users_repo(&self) -> &DynUsersRepository { &self.users_repo } + + pub fn secret_key(&self) -> &str { + &self.secret_key + } } diff --git a/link-for-later/src/types.rs b/link-for-later/src/types.rs index 7baebf2..a7eaa1d 100644 --- a/link-for-later/src/types.rs +++ b/link-for-later/src/types.rs @@ -2,7 +2,9 @@ pub use self::errors::App as AppError; pub use self::request::{ Login as LoginRequest, PostLink as PostLinkRequest, Register as RegisterRequest, }; +pub use self::response::Login as LoginResponse; +pub mod auth; pub mod entity; pub mod errors; pub mod request; diff --git a/link-for-later/src/types/auth.rs b/link-for-later/src/types/auth.rs new file mode 100644 index 0000000..679fe70 --- /dev/null +++ b/link-for-later/src/types/auth.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Token { + jwt: String, +} + +impl Claims { + pub fn new(email: &str) -> Self { + Self { + email: email.to_string(), + } + } +} + +impl Token { + pub fn new(jwt: &str) -> Self { + Self { + jwt: jwt.to_string(), + } + } + + pub fn jwt(&self) -> &str { + &self.jwt + } +} diff --git a/link-for-later/src/types/errors.rs b/link-for-later/src/types/errors.rs index c59e4a9..662c984 100644 --- a/link-for-later/src/types/errors.rs +++ b/link-for-later/src/types/errors.rs @@ -3,11 +3,12 @@ use std::{error, fmt}; #[derive(Debug)] pub enum App { NotSupported, - + ServerError, DatabaseError, ItemNotFound, UserAlreadyExists, UserNotFound, + InvalidPassword, #[cfg(test)] TestError, @@ -17,10 +18,12 @@ impl fmt::Display for App { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::NotSupported => write!(f, "operation not supported"), + Self::ServerError => write!(f, "server error"), Self::DatabaseError => write!(f, "database error"), Self::ItemNotFound => write!(f, "item not found"), Self::UserAlreadyExists => write!(f, "user already exist"), Self::UserNotFound => write!(f, "user not found"), + Self::InvalidPassword => write!(f, "incorrect password for user"), #[cfg(test)] Self::TestError => write!(f, "test error"), } diff --git a/link-for-later/src/types/response.rs b/link-for-later/src/types/response.rs index 6187eca..8175cc1 100644 --- a/link-for-later/src/types/response.rs +++ b/link-for-later/src/types/response.rs @@ -3,18 +3,24 @@ use axum::{ response::{IntoResponse, Response}, Json, }; +use serde::{Deserialize, Serialize}; use serde_json::json; -use super::AppError; +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Login { + pub token: String, +} -impl IntoResponse for AppError { +impl IntoResponse for super::AppError { fn into_response(self) -> Response { let (status, error_message) = match self { Self::NotSupported => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + Self::ServerError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), Self::DatabaseError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), Self::ItemNotFound => (StatusCode::NOT_FOUND, self.to_string()), Self::UserAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()), Self::UserNotFound => (StatusCode::BAD_REQUEST, self.to_string()), + Self::InvalidPassword => (StatusCode::UNAUTHORIZED, self.to_string()), #[cfg(test)] Self::TestError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), }; From 9e86d000b248c6ccb0666187ee5999c7a89da1ea Mon Sep 17 00:00:00 2001 From: Kent Tristan Yves Sarmiento Date: Thu, 21 Dec 2023 06:56:22 +0000 Subject: [PATCH 2/2] fix variable used --- link-for-later/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/link-for-later/src/state.rs b/link-for-later/src/state.rs index e540aba..c10beb0 100644 --- a/link-for-later/src/state.rs +++ b/link-for-later/src/state.rs @@ -20,7 +20,7 @@ impl AppState { links_repo: DynLinksRepository, users_repo: DynUsersRepository, ) -> Self { - let secret_key = std::env::var("MONGODB_URI").map_or_else(|_| String::new(), |key| key); + let secret_key = std::env::var("JWT_SECRET").map_or_else(|_| String::new(), |key| key); Self { links_service, users_service,