From 093e29022109dd891d238bd7572a53f42ab815c8 Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Sun, 23 Mar 2025 01:19:17 +0700 Subject: [PATCH] feat: complete auth system --- src/apps/v1/auth/auth_controller.rs | 24 ++++- src/apps/v1/auth/auth_dto.rs | 6 ++ src/apps/v1/auth/auth_repository.rs | 47 ++++----- src/apps/v1/auth/auth_service.rs | 131 +++++++++++++++----------- src/apps/v1/auth/mod.rs | 9 +- src/apps/v1/docs/docs_controller.rs | 8 +- src/apps/v1/users/users_repository.rs | 25 +++-- src/apps/v1/users/users_schema.rs | 1 - src/libs/jsonwebtoken/mod.rs | 16 ++++ src/utils/get_id.rs | 11 +++ src/utils/make_thing.rs | 5 + src/utils/mod.rs | 4 + 12 files changed, 186 insertions(+), 101 deletions(-) create mode 100644 src/utils/get_id.rs create mode 100644 src/utils/make_thing.rs diff --git a/src/apps/v1/auth/auth_controller.rs b/src/apps/v1/auth/auth_controller.rs index 0c16988..d267c48 100644 --- a/src/apps/v1/auth/auth_controller.rs +++ b/src/apps/v1/auth/auth_controller.rs @@ -1,6 +1,6 @@ use super::{ - AuthLoginRequestDto, AuthRegisterRequestDto, AuthResendOtpRequestDto, AuthService, - AuthVerifyEmailRequestDto, + AuthLoginRequestDto, AuthRefreshTokenRequestDto, AuthRegisterRequestDto, + AuthResendOtpRequestDto, AuthService, AuthVerifyEmailRequestDto, }; use crate::{v1::AuthLoginResponsetDto, AppState}; use crate::{AuthNewPasswordRequestDto, MessageResponseDto, ResponseSuccessDto}; @@ -42,7 +42,7 @@ pub async fn post_register( #[utoipa::path( post, - path = "/v1/auth/verify", + path = "/v1/auth/verify-email", request_body = AuthVerifyEmailRequestDto, responses( (status = 200, description = "Verify email successful", body = MessageResponseDto), @@ -59,7 +59,7 @@ pub async fn post_verify_email( #[utoipa::path( post, - path = "/v1/auth/resend", + path = "/v1/auth/send-otp", request_body = AuthResendOtpRequestDto, responses( (status = 200, description = "Resend otp successful", body = MessageResponseDto), @@ -107,3 +107,19 @@ pub async fn post_new_password( ) -> impl IntoResponse { AuthService::mutation_new_password(payload, &state).await } + +#[utoipa::path( + post, + path = "/v1/auth/refresh", + request_body = AuthRefreshTokenRequestDto, + responses( + (status = 200, description = "Refresh token request successful", body = MessageResponseDto), + (status = 401, description = "Refresh token request failed", body = MessageResponseDto) + ), + tag = "Authentication" +)] +pub async fn post_refresh_token( + Json(payload): Json, +) -> impl IntoResponse { + AuthService::mutation_refresh_token(payload).await +} diff --git a/src/apps/v1/auth/auth_dto.rs b/src/apps/v1/auth/auth_dto.rs index d87234a..c28006f 100644 --- a/src/apps/v1/auth/auth_dto.rs +++ b/src/apps/v1/auth/auth_dto.rs @@ -97,6 +97,12 @@ pub struct AuthResendOtpRequestDto { pub email: String, } +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct AuthRefreshTokenRequestDto { + #[validate(length(min = 1, message = "Refresh token cannot be empty"))] + pub refresh_token: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct AuthNewPasswordRequestDto { pub token: String, diff --git a/src/apps/v1/auth/auth_repository.rs b/src/apps/v1/auth/auth_repository.rs index 4fa1687..960079a 100644 --- a/src/apps/v1/auth/auth_repository.rs +++ b/src/apps/v1/auth/auth_repository.rs @@ -1,9 +1,8 @@ -use crate::{AppState, ResourceEnum, UsersSchema}; +use super::AuthOtpSchema; +use crate::{make_thing, AppState, ResourceEnum, UsersSchema}; use anyhow::{anyhow, bail, Result}; use chrono::{Duration, Utc}; -use super::AuthOtpSchema; - pub struct AuthRepository<'a> { state: &'a AppState, } @@ -14,12 +13,16 @@ impl<'a> AuthRepository<'a> { } pub async fn query_store_user(&self, user: UsersSchema) -> Result { - let user_clone = user.clone(); + let table = ResourceEnum::UsersCache.to_string(); + let user_id = user.email.clone(); + let id = make_thing(&table, &user_id); + let mut user_to_store = user.clone(); + user_to_store.id = id.clone(); let record: Option = self .state .surrealdb_mem - .update((ResourceEnum::UsersCache.to_string(), user.email)) - .content(user_clone) + .create((table, user_id)) + .content(user_to_store) .await?; match record { Some(_) => Ok("Success store user data".to_string()), @@ -52,34 +55,32 @@ impl<'a> AuthRepository<'a> { } pub async fn query_get_stored_otp(&self, email: String) -> Result { - let otp: Option = self - .state - .surrealdb_mem - .select((ResourceEnum::OtpCache.to_string(), &email)) - .await?; - match otp { - Some(data) => { - if Utc::now() > data.expires_at { - let _: Option = self + let table = ResourceEnum::OtpCache.to_string(); + let key = (table.as_str(), email.as_str()); + let result: Option = self.state.surrealdb_mem.select(key).await?; + match result { + Some(data) => match Utc::now() > data.expires_at { + true => { + let _ = self .state .surrealdb_mem - .delete((ResourceEnum::OtpCache.to_string(), &email)) + .delete::>(key) .await?; Err(anyhow!("OTP expired")) - } else { - Ok(data.otp) } - } - None => Err(anyhow!("No stored OTP found")), + false => Ok(data.otp), + }, + None => bail!("No stored OTP found"), } } pub async fn query_store_otp(&self, email: String, otp: u32) -> Result { - let expires_at = Utc::now() + Duration::seconds(300); // 5 menit + let expires_at = Utc::now() + Duration::seconds(300); + let table = ResourceEnum::OtpCache.to_string(); let record: Option = self .state .surrealdb_mem - .create((ResourceEnum::OtpCache.to_string(), email)) + .create((table.as_str(), email.as_str())) .content(AuthOtpSchema { otp, expires_at }) .await?; match record { @@ -89,7 +90,7 @@ impl<'a> AuthRepository<'a> { } pub async fn query_delete_stored_otp(&self, email: String) -> Result { - let record: Option = self + let record: Option = self .state .surrealdb_mem .delete((ResourceEnum::OtpCache.to_string(), email)) diff --git a/src/apps/v1/auth/auth_service.rs b/src/apps/v1/auth/auth_service.rs index f07c349..cdb47d4 100644 --- a/src/apps/v1/auth/auth_service.rs +++ b/src/apps/v1/auth/auth_service.rs @@ -1,14 +1,14 @@ use super::{ AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto, - AuthRegisterRequestDto, AuthRepository, AuthResendOtpRequestDto, - AuthVerifyEmailRequestDto, TokenDto, + AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository, + AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto, }; use crate::{ - common_response, encode_access_token, encode_refresh_token, extract_email_token, - generate_otp, get_iso_date, hash_password, send_email, success_response, - validate_request, verify_password, AppState, Env, ResourceEnum, - ResponseSuccessDto, UsersActiveInactiveSchema, UsersItemDto, UsersRepository, - UsersSchema, + common_response, decode_refresh_token, encode_access_token, encode_refresh_token, + encode_reset_password_token, extract_email_token, generate_otp, get_iso_date, + hash_password, send_email, success_response, validate_request, verify_password, + AppState, Env, ResourceEnum, ResponseSuccessDto, UsersActiveInactiveSchema, + UsersItemDto, UsersRepository, UsersSchema, UsersSetNewPasswordSchema, }; use axum::{http::StatusCode, response::Response}; use surrealdb::{ @@ -83,8 +83,8 @@ impl AuthService { }, }; - if let Err(_) = auth_repo.query_store_user(user).await { - return common_response(StatusCode::BAD_REQUEST, "Failed to store data"); + if let Err(err) = auth_repo.query_store_user(user).await { + return common_response(StatusCode::BAD_REQUEST, &err.to_string()); } success_response(response) @@ -200,6 +200,46 @@ impl AuthService { } } + pub async fn mutation_refresh_token( + payload: AuthRefreshTokenRequestDto, + ) -> Response { + let email = match decode_refresh_token(&payload.refresh_token) { + Ok(token) => token.claims.sub, + Err(_) => { + return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token"); + } + }; + + let access_token = match encode_access_token(email.clone()) { + Ok(token) => token, + Err(_) => { + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate access token", + ); + } + }; + + let refresh_token = match encode_refresh_token(email.clone()) { + Ok(token) => token, + Err(_) => { + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate refresh token", + ); + } + }; + + let response = ResponseSuccessDto { + data: TokenDto { + access_token, + refresh_token, + }, + }; + + success_response(response) + } + pub async fn mutation_forgot_password( payload: AuthResendOtpRequestDto, state: &AppState, @@ -212,7 +252,7 @@ impl AuthService { { return common_response(StatusCode::BAD_REQUEST, "User not found"); } - let token = match encode_access_token(payload.email.clone()) { + let token = match encode_reset_password_token(payload.email.clone()) { Ok(token) => token, Err(_) => { return common_response( @@ -240,46 +280,32 @@ impl AuthService { ) -> Response { let user_repo = UsersRepository::new(state); let auth_repo = AuthRepository::new(state); - - match auth_repo.query_get_stored_otp(payload.email.clone()).await { - Ok(stored_otp) => { - let user_otp = payload.otp; - let is_otp_valid = stored_otp == user_otp; - if is_otp_valid { - match user_repo - .query_active_inactive_user( - payload.email.clone(), - UsersActiveInactiveSchema { is_active: true }, - ) - .await - { - Ok(_) => { - if let Err(e) = auth_repo - .query_delete_stored_otp(payload.email.clone()) - .await - { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &e.to_string(), - ); - } - common_response(StatusCode::OK, "Email verified successfully") + let email = payload.email.clone(); + match auth_repo.query_get_stored_otp(email.clone()).await { + Ok(stored_otp) => match stored_otp == payload.otp { + true => match user_repo + .query_active_inactive_user( + email.clone(), + UsersActiveInactiveSchema { is_active: true }, + ) + .await + { + Ok(_) => match auth_repo.query_delete_stored_otp(email).await { + Ok(_) => common_response(StatusCode::OK, "Email verified successfully"), + Err(e) => { + common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()) } - Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), - } - } else { - if let Err(e) = auth_repo - .query_delete_stored_otp(payload.email.clone()) - .await - { - return common_response( - StatusCode::INTERNAL_SERVER_ERROR, - &format!("Failed to delete OTP: {}", e), - ); - } - common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP") - } - } + }, + Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), + }, + false => match auth_repo.query_delete_stored_otp(email).await { + Ok(_) => common_response(StatusCode::BAD_REQUEST, "Failed to verify OTP"), + Err(e) => common_response( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("Failed to delete OTP: {}", e), + ), + }, + }, Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), } } @@ -290,12 +316,9 @@ impl AuthService { ) -> Response { let user_repo = UsersRepository::new(state); let email = extract_email_token(payload.token).unwrap(); - + let password = hash_password(&payload.password).unwrap(); match user_repo - .query_update_password_user(crate::UsersSetNewPasswordSchema { - email: email.clone(), - password: payload.password.clone(), - }) + .query_update_password_user(email, UsersSetNewPasswordSchema { password }) .await { Ok(msg) => common_response(StatusCode::OK, &msg), diff --git a/src/apps/v1/auth/mod.rs b/src/apps/v1/auth/mod.rs index dd94e19..8443a53 100644 --- a/src/apps/v1/auth/mod.rs +++ b/src/apps/v1/auth/mod.rs @@ -14,10 +14,11 @@ pub use auth_service::*; pub fn auth_router() -> Router { Router::new() - .route("/login", post(auth_controller::post_login)) - .route("/register", post(auth_controller::post_register)) - .route("/verify", post(auth_controller::post_verify_email)) - .route("/resend", post(auth_controller::post_resend_otp)) .route("/forgot", post(auth_controller::post_forgot_password)) + .route("/login", post(auth_controller::post_login)) .route("/new-password", post(auth_controller::post_new_password)) + .route("/refresh", post(auth_controller::post_refresh_token)) + .route("/register", post(auth_controller::post_register)) + .route("/send-otp", post(auth_controller::post_resend_otp)) + .route("/verify-email", post(auth_controller::post_verify_email)) } diff --git a/src/apps/v1/docs/docs_controller.rs b/src/apps/v1/docs/docs_controller.rs index 25336f4..ea97f96 100644 --- a/src/apps/v1/docs/docs_controller.rs +++ b/src/apps/v1/docs/docs_controller.rs @@ -3,8 +3,8 @@ use crate::{ auth, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, }, - AuthNewPasswordRequestDto, MessageResponseDto, MetaRequestDto, MetaResponseDto, - ResponseSuccessDto, + AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, MessageResponseDto, + MetaRequestDto, MetaResponseDto, ResponseSuccessDto, TokenDto, }; use utoipa::{ @@ -19,6 +19,8 @@ use utoipa::{ auth::auth_controller::post_register, auth::auth_controller::post_verify_email, auth::auth_controller::post_resend_otp, + auth::auth_controller::post_refresh_token, + auth::auth_controller::post_forgot_password, auth::auth_controller::post_new_password ), components( @@ -31,6 +33,8 @@ use utoipa::{ AuthVerifyEmailRequestDto, AuthResendOtpRequestDto, AuthNewPasswordRequestDto, + AuthRefreshTokenRequestDto, + ResponseSuccessDto, ResponseSuccessDto, ) ), diff --git a/src/apps/v1/users/users_repository.rs b/src/apps/v1/users/users_repository.rs index a3a95d2..35ecd5b 100644 --- a/src/apps/v1/users/users_repository.rs +++ b/src/apps/v1/users/users_repository.rs @@ -1,5 +1,5 @@ use super::{UsersActiveInactiveSchema, UsersSchema, UsersSetNewPasswordSchema}; -use crate::{AppState, AuthOtpSchema, ResourceEnum}; +use crate::{get_id, AppState, ResourceEnum}; use anyhow::{bail, Result}; pub struct UsersRepository<'a> { @@ -55,10 +55,8 @@ impl<'a> UsersRepository<'a> { pub async fn query_update_user(&self, data: UsersSchema) -> Result { let db = &self.state.surrealdb_ws; - let record: Option = db - .update((ResourceEnum::Users.to_string(), &data.id.id.to_string())) - .merge(data) - .await?; + let record_key = get_id(&data.id)?; + let record: Option = db.update(record_key).merge(data).await?; match record { Some(_) => Ok("Success update user".into()), None => bail!("Failed to update user"), @@ -72,29 +70,30 @@ impl<'a> UsersRepository<'a> { ) -> Result { let db = &self.state.surrealdb_ws; let user = self.query_user_by_email(email.clone()).await?; - let table = user.id.tb.as_str(); - let id = user.id.id.to_string(); - let result: Option = db - .update((table, id)) + let record_key = get_id(&user.id)?; + let record: Option = db + .update(record_key) .merge(UsersActiveInactiveSchema { is_active: data.is_active, }) .await?; - match result { - Some(_) => Ok("Success update user".to_string()), + match record { + Some(_) => Ok("Success update user".into()), None => bail!("Failed to update user"), } } pub async fn query_update_password_user( &self, + email: String, data: UsersSetNewPasswordSchema, ) -> Result { let db = &self.state.surrealdb_ws; + let user = self.query_user_by_email(email).await?; + let record_key = get_id(&user.id)?; let record: Option = db - .update((ResourceEnum::Users.to_string(), &data.email)) + .update(record_key) .merge(UsersSetNewPasswordSchema { - email: data.email.clone(), password: data.password.clone(), }) .await?; diff --git a/src/apps/v1/users/users_schema.rs b/src/apps/v1/users/users_schema.rs index 53ae4b9..84299ed 100644 --- a/src/apps/v1/users/users_schema.rs +++ b/src/apps/v1/users/users_schema.rs @@ -60,7 +60,6 @@ impl Default for UsersSchema { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UsersSetNewPasswordSchema { - pub email: String, pub password: String, } diff --git a/src/libs/jsonwebtoken/mod.rs b/src/libs/jsonwebtoken/mod.rs index 22da1e9..5fb358f 100644 --- a/src/libs/jsonwebtoken/mod.rs +++ b/src/libs/jsonwebtoken/mod.rs @@ -29,6 +29,22 @@ pub fn encode_access_token(sub: String) -> Result { .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) } +pub fn encode_reset_password_token(sub: String) -> Result { + let env = Env::new(); + let secret: String = env.access_token_secret; + let now = Utc::now(); + let expire: TimeDelta = Duration::minutes(5); + let exp: usize = (now + expire).timestamp() as usize; + let iat: usize = now.timestamp() as usize; + let claim = Claims { iat, exp, sub }; + encode( + &Header::default(), + &claim, + &EncodingKey::from_secret(secret.as_ref()), + ) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) +} + pub fn decode_access_token( jwt_token: &str, ) -> Result, StatusCode> { diff --git a/src/utils/get_id.rs b/src/utils/get_id.rs new file mode 100644 index 0000000..65018d8 --- /dev/null +++ b/src/utils/get_id.rs @@ -0,0 +1,11 @@ +use anyhow::{bail, Result}; +use surrealdb::sql::Thing; + +pub fn get_id(thing: &Thing) -> Result<(&str, &str)> { + let table = thing.tb.as_str(); + let id = match &thing.id { + surrealdb::sql::Id::String(s) => s.as_str(), + _ => bail!("Unsupported ID type"), + }; + Ok((table, id)) +} diff --git a/src/utils/make_thing.rs b/src/utils/make_thing.rs new file mode 100644 index 0000000..39986ab --- /dev/null +++ b/src/utils/make_thing.rs @@ -0,0 +1,5 @@ +use surrealdb::sql::Thing; + +pub fn make_thing(table: &str, id: &str) -> Thing { + Thing::from((table, id)) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4099c5f..862815c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,11 +1,15 @@ pub mod extract_email; pub mod generate_date; pub mod generate_otp; +pub mod get_id; +pub mod make_thing; pub mod response_format; pub mod validator; pub use extract_email::*; pub use generate_date::*; pub use generate_otp::*; +pub use get_id::*; +pub use make_thing::*; pub use response_format::*; pub use validator::*;