Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user login #20

Merged
merged 2 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions link-for-later/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 9 additions & 3 deletions link-for-later/src/controller/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,13 +39,19 @@ async fn login(
Json(payload): Json<LoginRequest>,
) -> 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()
Expand Down
4 changes: 3 additions & 1 deletion link-for-later/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use mockall::{automock, predicate::*};
use crate::{
repository,
types::{
auth::Token,
entity::{LinkItem, UserInfo},
Result,
},
Expand Down Expand Up @@ -50,8 +51,9 @@ pub trait Users {
async fn login(
&self,
users_repo: Box<repository::DynUsers>,
secret_key: &str,
user_info: &UserInfo,
) -> Result<UserInfo>;
) -> Result<Token>;
}

pub mod links;
Expand Down
30 changes: 25 additions & 5 deletions link-for-later/src/service/users.rs
Original file line number Diff line number Diff line change
@@ -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 {}
Expand Down Expand Up @@ -35,11 +40,26 @@ impl UsersService for ServiceProvider {
async fn login(
&self,
users_repo: Box<repository::DynUsers>,
secret_key: &str,
user_info: &UserInfo,
) -> Result<UserInfo> {
users_repo.find_by_user(&user_info.email).await?;
) -> Result<Token> {
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))
}
}
7 changes: 7 additions & 0 deletions link-for-later/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct AppState {
users_service: DynUsersService,
links_repo: DynLinksRepository,
users_repo: DynUsersRepository,
secret_key: String,
}

impl AppState {
Expand All @@ -19,11 +20,13 @@ impl AppState {
links_repo: DynLinksRepository,
users_repo: DynUsersRepository,
) -> Self {
let secret_key = std::env::var("JWT_SECRET").map_or_else(|_| String::new(), |key| key);
Self {
links_service,
users_service,
links_repo,
users_repo,
secret_key,
}
}

Expand All @@ -42,4 +45,8 @@ impl AppState {
pub fn users_repo(&self) -> &DynUsersRepository {
&self.users_repo
}

pub fn secret_key(&self) -> &str {
&self.secret_key
}
}
2 changes: 2 additions & 0 deletions link-for-later/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions link-for-later/src/types/auth.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
5 changes: 4 additions & 1 deletion link-for-later/src/types/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use std::{error, fmt};
#[derive(Debug)]
pub enum App {
NotSupported,

ServerError,
DatabaseError,
ItemNotFound,
UserAlreadyExists,
UserNotFound,
InvalidPassword,

#[cfg(test)]
TestError,
Expand All @@ -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"),
}
Expand Down
10 changes: 8 additions & 2 deletions link-for-later/src/types/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
};
Expand Down
Loading