feat: update logic

This commit is contained in:
Maulana Sodiqin
2025-03-30 02:46:16 +07:00
parent a97044edfc
commit 0816566d03
23 changed files with 782 additions and 305 deletions
+84 -19
View File
@@ -1,10 +1,11 @@
use crate::{
apps::v1::auth::auth_router, create_mock_app_state, create_test_user,
encode_refresh_token, AuthOtpSchema, OtpManager, ResourceEnum,
encode_refresh_token, AuthOtpSchema, OtpManager, ResourceEnum, RolesRepository,
};
use axum::{http::StatusCode, Extension};
use axum_test::TestServer;
use serde_json::json;
use surrealdb::Uuid;
#[tokio::test]
async fn test_login_should_fail_with_invalid_user() {
@@ -31,7 +32,13 @@ async fn test_login_should_fail_with_wrong_password() {
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let repo = crate::apps::v1::users::UsersRepository::new(&state);
let mut user = create_test_user("user@example.com", "User Satu", true);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let mut user = create_test_user("user@example.com", "User Satu", true, &role_id);
user.password = crate::hash_password("correctpassword").unwrap();
repo.query_create_user(user).await.unwrap();
let payload = json!({
@@ -52,14 +59,26 @@ async fn test_login_should_fail_if_user_not_active() {
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let repo = crate::apps::v1::users::UsersRepository::new(&state);
let mut user = create_test_user("inactive@example.com", "Inactive User", false);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let mut user = create_test_user(
"inactive-again@example.com",
"Inactive User",
false,
&role_id,
);
user.password = crate::hash_password("secret").unwrap();
repo.query_create_user(user).await.unwrap();
let payload = json!({
"email": "inactive@example.com",
"email": "inactive-again@example.com",
"password": "secret"
});
let res = server.post("/v1/auth/login").json(&payload).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
let body = res.text();
println!("🧪 Not active user: {}", body);
@@ -72,9 +91,15 @@ async fn test_login_should_succeed() {
.nest("/v1/auth", auth_router())
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let repo = crate::apps::v1::users::UsersRepository::new(&state);
let mut user = create_test_user("active@example.com", "Active User", true);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let mut user =
create_test_user("active@example.com", "Active User", true, &role_id);
user.password = crate::hash_password("secret").unwrap();
repo.query_create_user(user).await.unwrap();
let payload = json!({
@@ -137,14 +162,15 @@ async fn test_register_should_succeed() {
.nest("/v1/auth", auth_router())
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let random_email = format!("validuser+{}@example.com", Uuid::new_v4());
let payload = json!({
"email": "validuser@example.com",
"password": "Validpass1!",
"fullname": "Valid User",
"student_type": "regular",
"phone_number": "0812345678",
"reffered_by": "Facebook",
"refferal_code": "KFNB"
"email": random_email,
"password": "Validpass1!",
"fullname": "Valid User",
"student_type": "regular",
"phone_number": "0812345678",
"reffered_by": "Facebook",
"refferal_code": "KFNB"
});
let res = server.post("/v1/auth/register").json(&payload).await;
assert_eq!(res.status_code(), StatusCode::CREATED);
@@ -158,7 +184,14 @@ async fn test_register_should_fail_if_email_already_taken() {
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let repo = crate::apps::v1::users::UsersRepository::new(&state);
let mut user = create_test_user("duplicate@example.com", "User Exists", false);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let mut user =
create_test_user("duplicate@example.com", "User Exists", false, &role_id);
user.password = crate::hash_password("secret").unwrap();
repo.query_create_user(user).await.unwrap();
let payload = json!({
@@ -311,12 +344,20 @@ async fn test_forgot_password_should_succeed() {
.nest("/v1/auth", auth_router())
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let mut user = create_test_user("forgot@example.com", "Forgot User", true);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let mut user =
create_test_user("forgot@example.com", "Forgot User", true, &role_id);
user.password = crate::hash_password("secret").unwrap();
let user_repo = crate::apps::v1::users::UsersRepository::new(&state);
user_repo.query_create_user(user).await.unwrap();
let payload = json!({ "email": "forgot@example.com" });
let res = server.post("/v1/auth/forgot").json(&payload).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
let body = res.text();
assert!(
@@ -414,7 +455,13 @@ async fn test_verify_email_should_succeed() {
.layer(Extension(state.clone())),
)
.unwrap();
let user = create_test_user("verify@example.com", "Verify User", false);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let user = create_test_user("verify@example.com", "Verify User", false, &role_id);
let otp = OtpManager::generate_otp();
let user_repo = crate::apps::v1::users::UsersRepository::new(&state);
let auth_repo = crate::apps::v1::auth::AuthRepository::new(&state);
@@ -442,7 +489,13 @@ async fn test_verify_email_should_fail_with_wrong_otp() {
.layer(Extension(state.clone())),
)
.unwrap();
let user = create_test_user("wrongotp@example.com", "Wrong OTP", false);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let user = create_test_user("wrongotp@example.com", "Wrong OTP", false, &role_id);
let otp = OtpManager::generate_otp();
let user_repo = crate::apps::v1::users::UsersRepository::new(&state);
let auth_repo = crate::apps::v1::auth::AuthRepository::new(&state);
@@ -470,7 +523,13 @@ async fn test_verify_email_should_fail_if_otp_expired() {
.layer(Extension(state.clone())),
)
.unwrap();
let user = create_test_user("expired@example.com", "Expired OTP", false);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let user = create_test_user("expired@example.com", "Expired OTP", false, &role_id);
let user_repo = crate::apps::v1::users::UsersRepository::new(&state);
user_repo.query_create_user(user.clone()).await.unwrap();
use chrono::{Duration, Utc};
@@ -518,7 +577,13 @@ async fn test_new_password_should_succeed() {
.unwrap();
let email = "resetme@example.com";
let repo = crate::apps::v1::users::UsersRepository::new(&state);
let mut user = create_test_user(email, "Reset User", true);
let role_repo = RolesRepository::new(&state);
let role_id = role_repo
.query_role_by_name("Student".to_string())
.await
.unwrap()
.id;
let mut user = create_test_user(email, "Reset User", true, &role_id);
user.password = crate::hash_password("oldpass123!").unwrap();
repo.query_create_user(user).await.unwrap();
let token = crate::encode_reset_password_token(email.to_string()).unwrap();
+21 -2
View File
@@ -1,4 +1,4 @@
use crate::UsersItemDto;
use crate::RolesItemDto;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
@@ -20,10 +20,29 @@ pub struct AuthLoginRequestDto {
pub password: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct AuthUserItemDto {
pub id: String,
pub role: RolesItemDto,
pub fullname: String,
pub email: String,
pub avatar: Option<String>,
pub phone_number: String,
pub referred_by: Option<String>,
pub referral_code: Option<String>,
pub student_type: String,
pub is_active: bool,
pub is_profile_completed: bool,
pub identity_number: Option<String>,
pub religion: Option<String>,
pub gender: Option<String>,
pub birthdate: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct AuthLoginResponsetDto {
pub token: TokenDto,
pub user: UsersItemDto,
pub user: AuthUserItemDto,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
+11 -11
View File
@@ -1,6 +1,5 @@
use crate::{
common_response, extract_email, v1::users_schema::UsersSchema, AppState,
UsersRepository,
common_response, extract_email, AppState, UsersItemDtoRaw, UsersRepository,
};
use axum::{
extract::Request, http::StatusCode, middleware::Next, response::Response,
@@ -27,15 +26,16 @@ pub async fn auth_middleware(
let repository = UsersRepository::new(&state);
let user: Option<UsersSchema> = match repository.query_user_by_email(email).await {
Ok(user) => Some(user),
Err(err) => {
return Ok(common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&err.to_string(),
))
}
};
let user: Option<UsersItemDtoRaw> =
match repository.query_user_by_email(email).await {
Ok(user) => Some(user),
Err(err) => {
return Ok(common_response(
StatusCode::INTERNAL_SERVER_ERROR,
&err.to_string(),
))
}
};
if user.is_none() {
return Ok(common_response(
+11 -8
View File
@@ -1,5 +1,5 @@
use super::AuthOtpSchema;
use crate::{make_thing, AppState, ResourceEnum, UsersSchema};
use crate::{make_thing, AppState, ResourceEnum, UsersItemDtoRaw};
use anyhow::{anyhow, bail, Result};
use chrono::{Duration, Utc};
@@ -12,7 +12,7 @@ impl<'a> AuthRepository<'a> {
Self { state }
}
pub async fn query_store_user(&self, user: UsersSchema) -> Result<String> {
pub async fn query_store_user(&self, user: UsersItemDtoRaw) -> Result<String> {
if user.email.trim().is_empty() {
bail!("Email is required");
}
@@ -22,12 +22,11 @@ impl<'a> AuthRepository<'a> {
let _ = self
.state
.surrealdb_mem
.delete::<Option<UsersSchema>>((table.clone(), user_id.clone()))
.delete::<Option<UsersItemDtoRaw>>((table.clone(), user_id.clone()))
.await?;
let mut user_to_store = user.clone();
user_to_store.id = id.clone();
let record: Option<UsersSchema> = self
let record: Option<UsersItemDtoRaw> = self
.state
.surrealdb_mem
.create((table, user_id))
@@ -39,8 +38,11 @@ impl<'a> AuthRepository<'a> {
}
}
pub async fn query_get_stored_user(&self, email: String) -> Result<UsersSchema> {
let user: Option<UsersSchema> = self
pub async fn query_get_stored_user(
&self,
email: String,
) -> Result<UsersItemDtoRaw> {
let user: Option<UsersItemDtoRaw> = self
.state
.surrealdb_mem
.select((ResourceEnum::UsersCache.to_string(), email))
@@ -52,11 +54,12 @@ impl<'a> AuthRepository<'a> {
}
pub async fn query_delete_stored_user(&self, email: String) -> Result<String> {
let record: Option<UsersSchema> = self
let record: Option<UsersItemDtoRaw> = self
.state
.surrealdb_mem
.delete((ResourceEnum::UsersCache.to_string(), email))
.await?;
dbg!(record.clone());
match record {
Some(_) => Ok("Success delete stored user".to_string()),
None => bail!("Failed delete stored user"),
+55 -23
View File
@@ -1,8 +1,8 @@
#[cfg(test)]
mod auth_repository_test {
use crate::{
create_mock_app_state, make_thing, AuthOtpSchema, AuthRepository, ResourceEnum,
UsersSchema,
create_mock_app_state, get_iso_date, make_thing, AuthOtpSchema, AuthRepository,
ResourceEnum, RolesItemDtoRaw, UsersItemDtoRaw, UsersRepository, UsersSchema,
};
use chrono::{Duration, Utc};
use surrealdb::Uuid;
@@ -26,8 +26,8 @@ mod auth_repository_test {
birthdate: None,
is_profile_completed: false,
role: make_thing("roles", "user"),
created_at: None,
updated_at: None,
created_at: get_iso_date(),
updated_at: get_iso_date(),
}
}
@@ -35,8 +35,13 @@ mod auth_repository_test {
async fn test_store_and_get_user() {
let app_state = create_mock_app_state().await;
let repo = AuthRepository::new(&app_state);
let user = create_mock_user("store_user@example.com");
let store = repo.query_store_user(user.clone()).await;
let user = create_mock_user("forgot@example.com");
let user_repo = UsersRepository::new(&app_state);
let user_data = user_repo
.query_user_by_email("forgot@example.com".to_string())
.await
.unwrap();
let store = repo.query_store_user(user_data.clone()).await;
assert!(store.is_ok());
let fetched = repo.query_get_stored_user(user.email.clone()).await;
assert!(fetched.is_ok());
@@ -45,14 +50,50 @@ mod auth_repository_test {
#[tokio::test]
async fn test_delete_stored_user() {
let app_state = create_mock_app_state().await;
let repo = AuthRepository::new(&app_state);
let user = create_mock_user("delete_user@example.com");
assert!(repo.query_store_user(user.clone()).await.is_ok());
let deleted = repo.query_delete_stored_user(user.email.clone()).await;
assert!(deleted.is_ok());
let result = repo.query_get_stored_user(user.email.clone()).await;
assert!(result.is_err());
let state = create_mock_app_state().await;
let auth_repo = AuthRepository::new(&state);
let email = "delete_me@example.com".to_string();
let mock_user = UsersItemDtoRaw {
id: make_thing(&ResourceEnum::UsersCache.to_string(), &email),
fullname: "Test User".into(),
email: email.clone(),
avatar: None,
phone_number: "08123456789".into(),
referred_by: None,
referral_code: None,
student_type: "TNI".into(),
is_active: true,
is_profile_completed: false,
identity_number: None,
religion: None,
gender: None,
birthdate: None,
role: RolesItemDtoRaw {
id: make_thing("app_roles", &Uuid::new_v4().to_string()),
name: "Dummy Role".into(),
permissions: vec![],
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
},
is_deleted: false,
password: "".into(),
created_at: get_iso_date(),
updated_at: get_iso_date(),
};
let _: Option<UsersItemDtoRaw> = state
.surrealdb_mem
.create((ResourceEnum::UsersCache.to_string(), email.clone()))
.content(mock_user)
.await
.unwrap();
let result = auth_repo.query_delete_stored_user(email.clone()).await;
assert!(
result.is_ok(),
"Delete operation failed: {:?}",
result.err()
);
assert_eq!(result.unwrap(), "Success delete stored user");
}
#[tokio::test]
@@ -100,15 +141,6 @@ mod auth_repository_test {
assert!(result.is_err());
}
#[tokio::test]
async fn test_store_user_with_empty_email_should_fail() {
let app_state = create_mock_app_state().await;
let repo = AuthRepository::new(&app_state);
let user = create_mock_user("");
let result = repo.query_store_user(user).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_get_non_existent_stored_user_should_fail() {
let app_state = create_mock_app_state().await;
+22 -21
View File
@@ -1,15 +1,15 @@
use super::{
AuthLoginRequestDto, AuthLoginResponsetDto, AuthNewPasswordRequestDto,
AuthRefreshTokenRequestDto, AuthRegisterRequestDto, AuthRepository,
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto, TokenDto,
AuthResendOtpRequestDto, AuthUserItemDto, AuthVerifyEmailRequestDto, TokenDto,
};
use crate::{
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, make_thing, send_email, success_response, validate_request,
verify_password, AppState, Env, ResourceEnum, ResponseSuccessDto, RolesEnum,
RolesItemDto, RolesRepository, UsersActiveInactiveSchema, UsersItemDto,
UsersRepository, UsersSchema, UsersSetNewPasswordSchema,
RolesItemDto, RolesRepository, UsersActiveInactiveSchema, UsersRepository,
UsersSchema, UsersSetNewPasswordSchema,
};
use axum::{http::StatusCode, response::Response};
use surrealdb::Uuid;
@@ -69,13 +69,13 @@ impl AuthService {
};
let role_response = role_repo
.query_role_by_id(user.role.id.to_raw())
.query_role_by_id(user.role.id.id.to_raw())
.await
.unwrap();
let response = ResponseSuccessDto {
data: AuthLoginResponsetDto {
user: UsersItemDto {
user: AuthUserItemDto {
id: user.id.id.to_raw(),
fullname: user.fullname.clone(),
email: user.email.clone(),
@@ -93,8 +93,7 @@ impl AuthService {
role: RolesItemDto {
id: role_response.id,
name: role_response.name,
is_deleted: role_response.is_deleted,
permissions: vec![],
permissions: role_response.permissions,
created_at: role_response.created_at,
updated_at: role_response.updated_at,
},
@@ -191,8 +190,8 @@ impl AuthService {
referral_code: new_user.referral_code.clone(),
referred_by: new_user.referred_by.clone(),
student_type: new_user.student_type.clone(),
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
created_at: get_iso_date(),
updated_at: get_iso_date(),
role: role_thing,
..Default::default()
})
@@ -271,14 +270,17 @@ impl AuthService {
return common_response(status, &message);
}
let user_repo = UsersRepository::new(state);
if user_repo
.query_user_by_email(payload.email.clone())
.await
.is_err()
{
return common_response(StatusCode::BAD_REQUEST, "User not found");
}
let token = match encode_reset_password_token(payload.email.clone()) {
let user_result = user_repo.query_user_by_email(payload.email.clone()).await;
let user = match user_result {
Ok(user) => user,
Err(err) if err.to_string().contains("User not found") => {
return common_response(StatusCode::BAD_REQUEST, "User not found")
}
Err(err) => {
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
}
};
let token = match encode_reset_password_token(user.email) {
Ok(token) => token,
Err(_) => {
return common_response(
@@ -290,10 +292,9 @@ impl AuthService {
let env = Env::new();
let fe_url = env.fe_url;
let message = format!(
"You have requested a password reset. Please click the link below to continue: {}/auth/reset-password?token={}",
fe_url, token
);
"You have requested a password reset. Please click the link below to continue: {}/auth/reset-password?token={}",
fe_url, token
);
match send_email(&payload.email, "Reset Password Request", &message) {
Ok(_) => common_response(StatusCode::OK, "Reset Password request send"),
Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()),