From 9ffe80879716d5344faa2a6311da7105264923ba Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Fri, 14 Mar 2025 09:53:20 +0700 Subject: [PATCH] feat: basic auth --- src/apps/v1/auth/auth_controller.rs | 8 +- src/apps/v1/auth/auth_dto.rs | 3 + src/apps/v1/auth/auth_repository.rs | 59 ++++++----- src/apps/v1/auth/auth_service.rs | 153 ++++++++++++++++++---------- src/apps/v1/users/users_dto.rs | 1 - src/libs/surrealdb/mod.rs | 3 + src/libs/surrealdb/resource.rs | 19 ++++ 7 files changed, 166 insertions(+), 80 deletions(-) create mode 100644 src/libs/surrealdb/resource.rs diff --git a/src/apps/v1/auth/auth_controller.rs b/src/apps/v1/auth/auth_controller.rs index 8ae6f28..663aa22 100644 --- a/src/apps/v1/auth/auth_controller.rs +++ b/src/apps/v1/auth/auth_controller.rs @@ -1,6 +1,4 @@ -use super::{ - mutation_login, mutation_register, AuthLoginRequestDto, AuthRegisterRequestDto, -}; +use super::{AuthLoginRequestDto, AuthRegisterRequestDto, AuthService}; use crate::AppState; use axum::{response::IntoResponse, Extension, Json}; @@ -8,12 +6,12 @@ pub async fn post_login( Extension(state): Extension, Json(payload): Json, ) -> impl IntoResponse { - mutation_login(payload, &state).await + AuthService::mutation_login(payload, &state).await } pub async fn post_register( Extension(state): Extension, Json(payload): Json, ) -> impl IntoResponse { - mutation_register(payload, &state).await + AuthService::mutation_register(payload, &state).await } diff --git a/src/apps/v1/auth/auth_dto.rs b/src/apps/v1/auth/auth_dto.rs index d3790f8..71c180b 100644 --- a/src/apps/v1/auth/auth_dto.rs +++ b/src/apps/v1/auth/auth_dto.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use crate::v1::UsersItemDto; + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct AuthLoginRequestDto { pub email: String, @@ -10,6 +12,7 @@ pub struct AuthLoginRequestDto { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct AuthLoginResponsetDto { pub token: TokenDto, + pub user: UsersItemDto, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] diff --git a/src/apps/v1/auth/auth_repository.rs b/src/apps/v1/auth/auth_repository.rs index e67b4dc..34dfcfd 100644 --- a/src/apps/v1/auth/auth_repository.rs +++ b/src/apps/v1/auth/auth_repository.rs @@ -1,32 +1,45 @@ -use crate::{v1::UsersItemDto, AppState}; +use crate::{v1::UsersItemDto, AppState, ResourceEnum}; use std::error::Error; use super::AuthRegisterRequestDto; -pub async fn query_user_by_email( - email: String, - state: &AppState, -) -> Result> { - let db = &state.surrealdb; - - let mut result = db - .query("SELECT * FROM app_users WHERE email = $email LIMIT 1;") - .bind(("email", email.clone())) - .await?; - - let user: Option = result.take(0)?; - - user.ok_or_else(|| format!("User not found for email: {}", email).into()) +pub struct AuthRepository<'a> { + state: &'a AppState, } -pub async fn query_create_user( - data: AuthRegisterRequestDto, - state: &AppState, -) -> Result> { - let db = &state.surrealdb; +impl<'a> AuthRepository<'a> { + pub fn new(state: &'a AppState) -> Self { + Self { state } + } - let _record: Option = - db.create(("app_users", &data.email)).content(data).await?; + pub async fn query_user_by_email( + &self, + email: String, + ) -> Result> { + let db = &self.state.surrealdb; - Ok("Success create user".into()) + let result = db.select((ResourceEnum::Users.to_string(), email)).await?; + + match result { + Some(user) => Ok(user), + None => Err("User not found for email".into()), + } + } + + pub async fn query_create_user( + &self, + data: AuthRegisterRequestDto, + ) -> Result> { + let db = &self.state.surrealdb; + + let record: Option = db + .create((ResourceEnum::Users.to_string(), &data.email)) + .content(data) + .await?; + + match record { + Some(_) => Ok("Success create user".into()), + None => Err("Failed to create user".into()), + } + } } diff --git a/src/apps/v1/auth/auth_service.rs b/src/apps/v1/auth/auth_service.rs index 87a8cca..151374f 100644 --- a/src/apps/v1/auth/auth_service.rs +++ b/src/apps/v1/auth/auth_service.rs @@ -1,64 +1,115 @@ use axum::{http::StatusCode, response::Response}; +use redis::Commands; use super::{ - query_create_user, query_user_by_email, AuthLoginRequestDto, - AuthRegisterRequestDto, + AuthLoginRequestDto, AuthLoginResponsetDto, AuthRegisterRequestDto, + AuthRepository, TokenDto, +}; +use crate::{ + common_response, encode_access_token, encode_refresh_token, hash_password, + success_response, v1::UsersItemDto, verify_password, AppState, + ResponseSuccessDto, TokenSub, }; -use crate::{common_response, hash_password, verify_password, AppState}; -pub async fn mutation_login( - payload: AuthLoginRequestDto, - state: &AppState, -) -> Response { - match query_user_by_email(payload.email, state).await { - Ok(user) => { - let is_password_correct = - verify_password(&payload.password, &user.password).unwrap_or(false); +pub struct AuthService; - if is_password_correct { - common_response( - StatusCode::BAD_REQUEST, - "Email or password not correct", +impl AuthService { + pub async fn mutation_login( + payload: AuthLoginRequestDto, + state: &AppState, + ) -> Response { + let repository = AuthRepository::new(state); + match repository.query_user_by_email(payload.email.clone()).await { + Ok(user) => { + let is_password_correct = + verify_password(&payload.password, &user.password) + .unwrap_or(false); + + if is_password_correct { + common_response( + StatusCode::BAD_REQUEST, + "Email or password not correct", + ); + } + + let access_token = encode_access_token(TokenSub { + email: payload.email.clone(), + role_name: "Admin".to_string(), + }); + + let refresh_token = encode_refresh_token(TokenSub { + email: payload.email.clone(), + role_name: "Admin".to_string(), + }); + + let response = ResponseSuccessDto { + data: AuthLoginResponsetDto { + user: UsersItemDto { + fullname: user.fullname.clone(), + email: user.email.clone(), + }, + token: TokenDto { + access_token: access_token.unwrap(), + refresh_token: refresh_token.unwrap(), + }, + }, + }; + + let redis_key = + format!("authenticated_users_data:{}", payload.email.clone()); + + match state.redisdb.get_connection().and_then(|mut conn| { + conn.set_ex::<_, String, ()>( + &redis_key, + serde_json::to_string(&user).unwrap_or_default(), + 86400, + ) + }) { + Ok(_) => success_response(response), + Err(err) => common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("Redis storage failed: {}", err), + ), + } + } + Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()), + } + } + + pub async fn mutation_register( + payload: AuthRegisterRequestDto, + state: &AppState, + ) -> Response { + let repository = AuthRepository::new(state); + if repository + .query_user_by_email(payload.email.clone()) + .await + .is_ok() + { + return common_response(StatusCode::BAD_REQUEST, "User already exists"); + } + + let hashed_password = match hash_password(&payload.password) { + Ok(hash) => hash, + Err(_) => { + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to hash password", ); } + }; - common_response(StatusCode::OK, "Success Login") - } - Err(err) => common_response(StatusCode::UNAUTHORIZED, &err.to_string()), - } -} + let new_user = AuthRegisterRequestDto { + email: payload.email, + password: hashed_password, + fullname: payload.fullname, + }; -pub async fn mutation_register( - payload: AuthRegisterRequestDto, - state: &AppState, -) -> Response { - if query_user_by_email(payload.email.clone(), state) - .await - .is_ok() - { - return common_response(StatusCode::BAD_REQUEST, "User already exists"); - } - - let hashed_password = match hash_password(&payload.password) { - Ok(hash) => hash, - Err(_) => { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to hash password", - ); - } - }; - - let new_user = AuthRegisterRequestDto { - email: payload.email, - password: hashed_password, - fullname: payload.fullname, - }; - - match query_create_user(new_user, state).await { - Ok(_) => common_response(StatusCode::CREATED, "Registration successful"), - Err(err) => { - common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) + match repository.query_create_user(new_user).await { + Ok(_) => common_response(StatusCode::CREATED, "Registration successful"), + Err(err) => { + common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) + } } } } diff --git a/src/apps/v1/users/users_dto.rs b/src/apps/v1/users/users_dto.rs index d7e8bd0..3828896 100644 --- a/src/apps/v1/users/users_dto.rs +++ b/src/apps/v1/users/users_dto.rs @@ -5,5 +5,4 @@ use utoipa::ToSchema; pub struct UsersItemDto { pub email: String, pub fullname: String, - pub password: String, } diff --git a/src/libs/surrealdb/mod.rs b/src/libs/surrealdb/mod.rs index 8043026..b356c64 100644 --- a/src/libs/surrealdb/mod.rs +++ b/src/libs/surrealdb/mod.rs @@ -2,6 +2,9 @@ use crate::SurrealClient; use surrealdb::engine::remote::http::{Client, Http}; use surrealdb::{Result, Surreal}; +pub mod resource; +pub use resource::*; + pub async fn surrealdb_init() -> Result { let db = Surreal::::init(); db.connect::("localhost:8000").await?; diff --git a/src/libs/surrealdb/resource.rs b/src/libs/surrealdb/resource.rs new file mode 100644 index 0000000..a4f8c4e --- /dev/null +++ b/src/libs/surrealdb/resource.rs @@ -0,0 +1,19 @@ +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResourceEnum { + Users, + Roles, + Permissions, +} + +impl fmt::Display for ResourceEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + ResourceEnum::Users => "app_users", + ResourceEnum::Roles => "app_roles", + ResourceEnum::Permissions => "app_permissions", + }; + write!(f, "{}", str) + } +}