From 9d92a9a1361f906383fdceb527f1b316f0e968f9 Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Wed, 26 Mar 2025 13:06:34 +0700 Subject: [PATCH] feat: pagination --- src/apps/v1/auth/auth_repository.rs | 6 + src/apps/v1/auth/auth_service.rs | 44 ++-- .../v1/permissions/permissions_controller.rs | 12 ++ .../v1/permissions/permissions_repository.rs | 1 + src/apps/v1/roles/roles_controller.rs | 15 ++ src/apps/v1/roles/roles_repository.rs | 1 + src/apps/v1/users/users_controller.rs | 18 ++ src/apps/v1/users/users_dto.rs | 52 +++++ src/apps/v1/users/users_repository.rs | 77 ++++++- src/bin/api.rs | 9 + src/bin/seed_permissions.rs | 193 ++++++++++++++++++ src/bin/seed_roles.rs | 82 ++++++++ src/bin/test_babi.rs | 25 +++ src/utils/extract_email.rs | 18 +- src/utils/query_list.rs | 81 +++++--- 15 files changed, 573 insertions(+), 61 deletions(-) create mode 100644 src/bin/api.rs create mode 100644 src/bin/seed_permissions.rs create mode 100644 src/bin/seed_roles.rs create mode 100644 src/bin/test_babi.rs diff --git a/src/apps/v1/auth/auth_repository.rs b/src/apps/v1/auth/auth_repository.rs index a2b3c52..83b038f 100644 --- a/src/apps/v1/auth/auth_repository.rs +++ b/src/apps/v1/auth/auth_repository.rs @@ -19,6 +19,12 @@ impl<'a> AuthRepository<'a> { let table = ResourceEnum::UsersCache.to_string(); let user_id = user.email.clone(); let id = make_thing(&table, &user_id); + let _ = self + .state + .surrealdb_mem + .delete::>((table.clone(), user_id.clone())) + .await?; + let mut user_to_store = user.clone(); user_to_store.id = id.clone(); let record: Option = self diff --git a/src/apps/v1/auth/auth_service.rs b/src/apps/v1/auth/auth_service.rs index 68ea57d..9e0222a 100644 --- a/src/apps/v1/auth/auth_service.rs +++ b/src/apps/v1/auth/auth_service.rs @@ -8,8 +8,8 @@ use crate::{ 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, - RolesRepository, UsersActiveInactiveSchema, UsersItemDto, UsersRepository, - UsersSchema, UsersSetNewPasswordSchema, + RolesItemDto, RolesRepository, UsersActiveInactiveSchema, UsersItemDto, + UsersRepository, UsersSchema, UsersSetNewPasswordSchema, }; use axum::{http::StatusCode, response::Response}; use surrealdb::Uuid; @@ -27,6 +27,7 @@ impl AuthService { let user_repo = UsersRepository::new(state); let auth_repo = AuthRepository::new(state); + let role_repo = RolesRepository::new(state); match user_repo.query_user_by_email(payload.email.clone()).await { Ok(user) => { @@ -67,6 +68,11 @@ impl AuthService { } }; + let role_response = role_repo + .query_role_by_id(user.role.id.to_raw()) + .await + .unwrap(); + let response = ResponseSuccessDto { data: AuthLoginResponsetDto { user: UsersItemDto { @@ -84,6 +90,14 @@ impl AuthService { gender: user.gender.clone(), birthdate: user.birthdate.clone(), is_profile_completed: user.is_profile_completed.clone(), + role: RolesItemDto { + id: role_response.id, + name: role_response.name, + is_deleted: role_response.is_deleted, + permissions: vec![], + created_at: role_response.created_at, + updated_at: role_response.updated_at, + }, }, token: TokenDto { access_token, @@ -92,8 +106,8 @@ impl AuthService { }, }; - if let Err(err) = auth_repo.query_store_user(user).await { - return common_response(StatusCode::BAD_REQUEST, &err.to_string()); + if let Err(_err) = auth_repo.query_store_user(user).await { + return common_response(StatusCode::BAD_REQUEST, "User already login"); } success_response(response) @@ -109,16 +123,16 @@ impl AuthService { if let Err((status, message)) = validate_request(&payload) { return common_response(status, &message); } - let user_repo = UsersRepository::new(state); let auth_repo = AuthRepository::new(state); let role_repo = RolesRepository::new(state); - - let role = role_repo + let role = match role_repo .query_role_by_name(RolesEnum::Student.to_string()) .await - .unwrap(); - + { + Ok(role) => role, + Err(_) => return common_response(StatusCode::BAD_REQUEST, "Role Not Found"), + }; if user_repo .query_user_by_email(payload.email.clone()) .await @@ -126,7 +140,6 @@ impl AuthService { { return common_response(StatusCode::BAD_REQUEST, "User already exists"); } - let hashed_password = match hash_password(&payload.password) { Ok(hash) => hash, Err(_) => { @@ -136,7 +149,6 @@ impl AuthService { ); } }; - let new_user = AuthRegisterRequestDto { email: payload.email, password: hashed_password, @@ -146,9 +158,7 @@ impl AuthService { referral_code: payload.referral_code, referred_by: payload.referred_by, }; - let otp = generate_otp::OtpManager::generate_otp(); - match auth_repo .query_store_otp(new_user.email.clone(), otp.clone()) .await @@ -166,13 +176,11 @@ impl AuthService { return common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()); } } - let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &role.id); let user_thing = make_thing( &ResourceEnum::Users.to_string(), &Uuid::new_v4().to_string(), ); - match user_repo .query_create_user(UsersSchema { id: user_thing, @@ -186,8 +194,6 @@ impl AuthService { created_at: Some(get_iso_date()), updated_at: Some(get_iso_date()), role: role_thing, - is_active: false, - is_profile_completed: false, ..Default::default() }) .await @@ -230,7 +236,6 @@ impl AuthService { return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token"); } }; - let access_token = match encode_access_token(email.clone()) { Ok(token) => token, Err(_) => { @@ -240,7 +245,6 @@ impl AuthService { ); } }; - let refresh_token = match encode_refresh_token(email.clone()) { Ok(token) => token, Err(_) => { @@ -250,14 +254,12 @@ impl AuthService { ); } }; - let response = ResponseSuccessDto { data: TokenDto { access_token, refresh_token, }, }; - success_response(response) } diff --git a/src/apps/v1/permissions/permissions_controller.rs b/src/apps/v1/permissions/permissions_controller.rs index 4fb7a36..0e031b6 100644 --- a/src/apps/v1/permissions/permissions_controller.rs +++ b/src/apps/v1/permissions/permissions_controller.rs @@ -40,6 +40,9 @@ pub async fn get_permission_list( #[utoipa::path( get, path = "/v1/permissions/detail/{id}", + security( + ("Bearer" = []) + ), params(("id" = String, Path, description = "Permission ID")), responses( (status = 200, description = "Get permission by ID", body = ResponseSuccessDto) @@ -55,6 +58,9 @@ pub async fn get_permission_by_id( #[utoipa::path( post, + security( + ("Bearer" = []) + ), path = "/v1/permissions/create", request_body = PermissionsRequestDto, responses( @@ -71,6 +77,9 @@ pub async fn post_create_permission( #[utoipa::path( put, + security( + ("Bearer" = []) + ), path = "/v1/permissions/update/{id}", request_body = PermissionsRequestDto, responses( @@ -88,6 +97,9 @@ pub async fn put_update_permission( #[utoipa::path( delete, + security( + ("Bearer" = []) + ), path = "/v1/permissions/delete/{id}", responses( (status = 200, description = "Delete permission", body = MessageResponseDto) diff --git a/src/apps/v1/permissions/permissions_repository.rs b/src/apps/v1/permissions/permissions_repository.rs index af8e078..5ea7728 100644 --- a/src/apps/v1/permissions/permissions_repository.rs +++ b/src/apps/v1/permissions/permissions_repository.rs @@ -31,6 +31,7 @@ impl<'a> PermissionsRepository<'a> { &ResourceEnum::Permissions.to_string(), &meta, conditions, + None, ) .await } diff --git a/src/apps/v1/roles/roles_controller.rs b/src/apps/v1/roles/roles_controller.rs index dc1c503..b5d23cc 100644 --- a/src/apps/v1/roles/roles_controller.rs +++ b/src/apps/v1/roles/roles_controller.rs @@ -12,6 +12,9 @@ use crate::{ #[utoipa::path( get, + security( + ("Bearer" = []) + ), path = "/v1/roles", params( ("page" = Option, Query, description = "Page number"), @@ -36,6 +39,9 @@ pub async fn get_role_list( #[utoipa::path( get, + security( + ("Bearer" = []) + ), path = "/v1/roles/detail/{id}", params(("id" = String, Path, description = "Role ID")), responses( @@ -52,6 +58,9 @@ pub async fn get_role_by_id( #[utoipa::path( post, + security( + ("Bearer" = []) + ), path = "/v1/roles/create", request_body = RolesRequestCreateDto, responses( @@ -68,6 +77,9 @@ pub async fn post_create_role( #[utoipa::path( put, + security( + ("Bearer" = []) + ), path = "/v1/roles/update/{id}", request_body = RolesRequestUpdateDto, responses( @@ -85,6 +97,9 @@ pub async fn put_update_role( #[utoipa::path( delete, + security( + ("Bearer" = []) + ), path = "/v1/roles/delete/{id}", responses( (status = 200, description = "Delete role", body = MessageResponseDto) diff --git a/src/apps/v1/roles/roles_repository.rs b/src/apps/v1/roles/roles_repository.rs index 6ef2abc..90547f2 100644 --- a/src/apps/v1/roles/roles_repository.rs +++ b/src/apps/v1/roles/roles_repository.rs @@ -45,6 +45,7 @@ impl<'a> RolesRepository<'a> { &ResourceEnum::Roles.to_string(), &meta, conditions, + None, ) .await } diff --git a/src/apps/v1/users/users_controller.rs b/src/apps/v1/users/users_controller.rs index 49e4e85..a1ed51d 100644 --- a/src/apps/v1/users/users_controller.rs +++ b/src/apps/v1/users/users_controller.rs @@ -12,6 +12,9 @@ use super::UsersUpdateRequestDto; #[utoipa::path( get, + security( + ("Bearer" = []) + ), path = "/v1/users", params( ("page" = Option, Query, description = "Page number"), @@ -36,6 +39,9 @@ pub async fn get_user_list( #[utoipa::path( get, + security( + ("Bearer" = []) + ), path = "/v1/users/detail/{id}", params( ("id" = String, Path, description = "User ID") @@ -54,6 +60,9 @@ pub async fn get_user_by_id( #[utoipa::path( post, + security( + ("Bearer" = []) + ), path = "/v1/users/create", request_body = UsersCreateRequestDto, responses( @@ -70,6 +79,9 @@ pub async fn post_create_user( #[utoipa::path( put, + security( + ("Bearer" = []) + ), path = "/v1/users/update/{id}", request_body = UsersCreateRequestDto, responses( @@ -87,6 +99,9 @@ pub async fn put_update_user( #[utoipa::path( put, + security( + ("Bearer" = []) + ), path = "/v1/users/activate/{id}", request_body = UsersActiveInactiveRequestDto, responses( @@ -104,6 +119,9 @@ pub async fn patch_user_active_status( #[utoipa::path( delete, + security( + ("Bearer" = []) + ), path = "/v1/users/delete/{id}", responses( (status = 200, description = "Soft delete user", body = MessageResponseDto) diff --git a/src/apps/v1/users/users_dto.rs b/src/apps/v1/users/users_dto.rs index 099cab8..8ab6539 100644 --- a/src/apps/v1/users/users_dto.rs +++ b/src/apps/v1/users/users_dto.rs @@ -1,9 +1,12 @@ use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; use utoipa::ToSchema; use validator::Validate; +use crate::RolesItemDto; + lazy_static! { static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap(); } @@ -82,6 +85,7 @@ pub struct UsersUpdateRequestDto { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct UsersItemDto { pub id: String, + pub role: RolesItemDto, pub fullname: String, pub email: String, pub avatar: Option, @@ -97,6 +101,54 @@ pub struct UsersItemDto { pub birthdate: Option, } +#[derive(Debug, Serialize, Deserialize)] +pub struct UsersItemDtoRaw { + pub id: Thing, + pub role: Thing, + pub fullname: String, + pub email: String, + pub avatar: Option, + pub phone_number: String, + pub referred_by: Option, + pub referral_code: Option, + pub student_type: String, + pub is_active: bool, + pub is_profile_completed: bool, + pub identity_number: Option, + pub religion: Option, + pub gender: Option, + pub birthdate: Option, +} + +impl From for UsersItemDto { + fn from(raw: UsersItemDtoRaw) -> Self { + Self { + id: raw.id.id.to_string(), + role: RolesItemDto { + id: raw.role.id.to_string(), + name: "".into(), + is_deleted: false, + permissions: vec![], + created_at: None, + updated_at: None, + }, + fullname: raw.fullname, + email: raw.email, + avatar: raw.avatar, + phone_number: raw.phone_number, + referred_by: raw.referred_by, + referral_code: raw.referral_code, + student_type: raw.student_type, + is_active: raw.is_active, + is_profile_completed: raw.is_profile_completed, + identity_number: raw.identity_number, + religion: raw.religion, + gender: raw.gender, + birthdate: raw.birthdate, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct UsersActiveInactiveRequestDto { pub is_active: bool, diff --git a/src/apps/v1/users/users_repository.rs b/src/apps/v1/users/users_repository.rs index c54a6e2..897da9b 100644 --- a/src/apps/v1/users/users_repository.rs +++ b/src/apps/v1/users/users_repository.rs @@ -1,4 +1,7 @@ -use super::{UsersActiveInactiveSchema, UsersSchema, UsersSetNewPasswordSchema}; +use super::{ + UsersActiveInactiveSchema, UsersItemDto, UsersItemDtoRaw, UsersSchema, + UsersSetNewPasswordSchema, +}; use crate::{ get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, @@ -17,20 +20,80 @@ impl<'a> UsersRepository<'a> { pub async fn query_user_list( &self, meta: MetaRequestDto, - ) -> Result>> { + ) -> Result>> { let db = &self.state.surrealdb_ws; - let table = ResourceEnum::Users.to_string(); + let mut conditions = vec!["is_deleted = false".to_string()]; - if let Some(search) = &meta.search { + + if let Some(search) = meta.search.as_deref() { if !search.is_empty() { conditions.push("string::contains(fullname ?? '', $search)".to_string()); } } - if meta.filter_by.is_some() && meta.filter.is_some() { - let filter_by = meta.filter_by.as_ref().unwrap(); + + if let (Some(filter_by), Some(_filter)) = + (meta.filter_by.as_ref(), meta.filter.as_ref()) + { conditions.push(format!("{} = $filter", filter_by)); } - query_list_with_meta::(db, &table, &meta, conditions).await + + let where_clause = if !conditions.is_empty() { + format!("WHERE {}", conditions.join(" AND ")) + } else { + String::new() + }; + + let limit = meta.per_page.unwrap_or(10); + let start = (meta.page.unwrap_or(1) - 1) * limit; + + let select_query = format!( + " + SELECT + id, + role, + fullname, + email, + avatar, + phone_number, + referred_by, + referral_code, + student_type, + is_active, + is_profile_completed, + identity_number, + religion, + gender, + birthdate + FROM {} + {} + LIMIT {} START {} + FETCH role, role.permissions + ", + ResourceEnum::Users.to_string(), + where_clause, + limit, + start + ); + + let raw_result = query_list_with_meta::( + db, + &ResourceEnum::Users.to_string(), + &meta, + vec![], + Some(select_query), + ) + .await?; + + let converted = raw_result + .data + .into_iter() + .map(UsersItemDto::from) + .collect(); + + Ok(ResponseListSuccessDto { + data: converted, + meta: raw_result.meta, + }) } pub async fn query_user_by_email(&self, email: String) -> Result { diff --git a/src/bin/api.rs b/src/bin/api.rs new file mode 100644 index 0000000..facdbb8 --- /dev/null +++ b/src/bin/api.rs @@ -0,0 +1,9 @@ +use najm_course_api::{apps, axum_init}; + +#[tokio::main] +async fn main() { + axum_init(|surrealdb_ws, surrealdb_mem| async { + apps(surrealdb_ws, surrealdb_mem).await + }) + .await; +} diff --git a/src/bin/seed_permissions.rs b/src/bin/seed_permissions.rs new file mode 100644 index 0000000..3a373b2 --- /dev/null +++ b/src/bin/seed_permissions.rs @@ -0,0 +1,193 @@ +use najm_course_api::{get_iso_date, Env}; +use serde_json::json; +use std::error::Error; +use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let env = Env::new(); + let db = Surreal::new::(env.surrealdb_url).await?; + db.signin(Root { + username: &env.surrealdb_username, + password: &env.surrealdb_password, + }) + .await?; + db.use_ns(env.surrealdb_namespace) + .use_db(env.surrealdb_dbname) + .await?; + let permissions = vec![ + ( + "023e2dfe-93c3-4008-94a8-b5dff403f73b", + "Create Users", + Some("2025-01-29T06:08:23.838311+00"), + Some("2025-01-29T06:08:23.838312+00"), + ), + ( + "0269ed71-0ae0-4c43-ad29-e3d861d8f9a0", + "Create Permissions", + None, + None, + ), + ( + "299cb4d5-6556-4cc9-b6c1-32e6d31e0f9b", + "Update Permissions", + Some("2025-01-29T05:11:01.265+00"), + Some("2025-01-29T05:11:01.265001+00"), + ), + ( + "319ee593-ff0a-4f29-bbaf-9feb3174a3a2", + "Create Roles", + None, + None, + ), + ( + "319ee593-ff0a-4f29-bbaf-9feb3174a3a6", + "Read Detail Users", + None, + None, + ), + ( + "35b0d992-65c8-4b62-b030-e6e0320e4048", + "Delete Roles", + None, + None, + ), + ( + "4da8b434-89f9-4d91-85ae-eebd63cdbeda", + "Update Activate Users", + Some("2025-02-01T12:38:09.741726+00"), + Some("2025-02-01T12:38:09.741727+00"), + ), + ( + "529fe4da-7e20-4c76-8bc1-d7f7c121218f", + "Create Tests", + Some("2025-02-24T16:52:27.316909+00"), + Some("2025-02-24T16:52:27.316918+00"), + ), + ( + "73888d18-b3e9-4f62-95a5-ba2c0d69fccb", + "Read Detail Roles", + Some("2025-01-29T05:13:06.445925+00"), + Some("2025-01-29T10:31:46.408564+00"), + ), + ( + "79dfe6dc-748c-4b9c-9535-b896b391d676", + "Delete Tests", + Some("2025-02-24T16:52:27.455878+00"), + Some("2025-02-24T16:52:27.455888+00"), + ), + ( + "7c15e31d-36e2-49f9-97db-138c03fb0cf6", + "Read List Users", + Some("2025-01-28T15:02:41.772931+00"), + Some("2025-01-28T15:02:41.772933+00"), + ), + ( + "7d4b1379-4960-416a-b045-98cd82c0cac9", + "Read Detail Sessions", + Some("2025-02-24T16:52:26.886664+00"), + Some("2025-02-24T16:52:26.886673+00"), + ), + ( + "811d386b-e5f0-4e00-a164-f3d885197e30", + "Update Tests", + Some("2025-02-24T16:52:27.385216+00"), + Some("2025-02-24T16:52:27.385225+00"), + ), + ( + "8195eeb8-e64f-4172-aa57-596492c84a72", + "Read List Permissions", + Some("2025-01-28T15:05:28.6299+00"), + Some("2025-01-28T15:05:28.629901+00"), + ), + ( + "81eba91d-b8ab-44b9-bbfe-4e6da2f98952", + "Read List Tests", + Some("2025-02-24T16:52:27.179542+00"), + Some("2025-02-24T16:52:27.179551+00"), + ), + ( + "8cfd3b4d-0a41-456d-88e5-6c21cef1766a", + "Delete Sessions", + Some("2025-02-24T16:52:27.111123+00"), + Some("2025-02-24T16:52:27.111132+00"), + ), + ( + "9164ca6e-c7e3-4238-a15f-f36ab9577e7e", + "Read List Roles", + None, + None, + ), + ( + "96df0689-2ae9-4894-bf00-837c19415e5c", + "Delete Users", + Some("2025-02-02T06:52:05.195565+00"), + Some("2025-02-02T06:52:05.195565+00"), + ), + ( + "98b3dc4c-0124-461f-afcd-166637c5e6e8", + "Update Users", + Some("2025-01-29T05:34:40.621554+00"), + Some("2025-01-29T05:34:40.621555+00"), + ), + ( + "a00d5608-4c48-4542-845c-dfe004687022", + "Update Roles", + None, + None, + ), + ( + "b2dc3928-86ba-4c59-a03d-0b57d5183ebc", + "Delete Permissions", + Some("2025-01-29T05:14:22.511084+00"), + Some("2025-01-29T05:14:22.511085+00"), + ), + ( + "b70733db-b1c8-4aa3-a10f-b7cf773d896b", + "Create Sessions", + Some("2025-02-24T16:52:26.974279+00"), + Some("2025-02-24T16:52:26.97429+00"), + ), + ( + "c0a31b2c-3f0c-4e82-b018-e60ba8674112", + "Update Sessions", + Some("2025-02-24T16:52:27.042858+00"), + Some("2025-02-24T16:52:27.042866+00"), + ), + ( + "cab6aff5-e9c6-4ed3-afe9-93ef927e1f92", + "Read List Sessions", + Some("2025-02-22T15:38:09.521014+00"), + Some("2025-02-22T15:38:25.964821+00"), + ), + ( + "dad435cf-042c-41bd-a946-cea61ed2ffbc", + "Read Detail Permissions", + Some("2025-01-28T15:07:10.990214+00"), + Some("2025-01-28T15:07:10.990214+00"), + ), + ( + "f768aff5-8011-4439-b901-d8793c60d841", + "Read Detail Tests", + Some("2025-02-24T16:52:27.2483+00"), + Some("2025-02-24T16:52:27.248308+00"), + ), + ]; + for (id, name, _created_at, _updated_at) in permissions { + db.query("CREATE type::thing('app_permissions', $id) CONTENT $data") + .bind(("id", id)) + .bind(( + "data", + json!({ + "name": name, + "is_deleted": false, + "created_at": get_iso_date(), + "updated_at": get_iso_date() + }), + )) + .await?; + println!("โœ… Inserted: {}", name); + } + println!("โœ… Semua permissions berhasil disimpan ke SurrealDB!"); + Ok(()) +} diff --git a/src/bin/seed_roles.rs b/src/bin/seed_roles.rs new file mode 100644 index 0000000..4425895 --- /dev/null +++ b/src/bin/seed_roles.rs @@ -0,0 +1,82 @@ +use najm_course_api::{get_iso_date, Env}; +use serde_json::json; +use std::error::Error; +use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let env = Env::new(); + let db = Surreal::new::(env.surrealdb_url).await?; + db.signin(Root { + username: &env.surrealdb_username, + password: &env.surrealdb_password, + }) + .await?; + db.use_ns(env.surrealdb_namespace) + .use_db(env.surrealdb_dbname) + .await?; + + let roles = vec![ + ( + "50133429-f4b1-4249-9f97-7b86e6ee9d86", + "Staf", + Some("2025-02-24T16:52:27.630453+00"), + Some("2025-02-24T16:52:27.630461+00"), + ), + ( + "5713cb37-dc02-4e87-8048-d7a41d352059", + "Student", + None, + Some("2025-02-28T14:53:58.576688+00"), + ), + ( + "60f1aeb7-dad2-4e06-bcb5-be1ba510c906", + "Staff Aktivasi User", + Some("2025-02-20T02:47:09.660640+00"), + Some("2025-02-20T02:48:30.083283+00"), + ), + ( + "6d4fea5d-4a08-4b8a-9782-f2ab2183dcf0", + "Admin Pembayaran", + Some("2025-01-29T05:39:28.562667+00"), + Some("2025-03-12T22:56:29.597416+00"), + ), + ( + "aec758fc-3d54-4c6f-8bcb-44368dd81c07", + "Admin Management Soal", + Some("2025-01-29T11:03:40.308934+00"), + Some("2025-01-29T11:03:40.308935+00"), + ), + ( + "de29943b-ed94-451e-a91f-bcf496bd1849", + "Admin Penilaian", + Some("2025-01-29T11:04:03.499272+00"), + Some("2025-01-29T16:14:09.715234+00"), + ), + ( + "f6b03f25-e416-4893-ac88-caaa690afb07", + "Admin", + None, + Some("2025-02-22T15:38:39.868306+00"), + ), + ]; + + for (id, name, _created_at, _updated_at) in roles { + db.query("CREATE type::thing('app_roles', $id) CONTENT $data") + .bind(("id", id)) + .bind(( + "data", + json!({ + "name": name, + "permissions": [], + "is_deleted": false, + "created_at": get_iso_date(), + "updated_at": get_iso_date(), + }), + )) + .await?; + println!("โœ… Inserted role: {}", name); + } + println!("โœ… Semua role berhasil disimpan ke SurrealDB!"); + Ok(()) +} diff --git a/src/bin/test_babi.rs b/src/bin/test_babi.rs new file mode 100644 index 0000000..da40f3a --- /dev/null +++ b/src/bin/test_babi.rs @@ -0,0 +1,25 @@ +use najm_course_api::{Env, UsersItemDto, UsersItemDtoRaw}; +use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let env = Env::new(); + let db = Surreal::new::(env.surrealdb_url).await?; + db.signin(Root { + username: &env.surrealdb_username, + password: &env.surrealdb_password, + }) + .await?; + db.use_ns(env.surrealdb_namespace) + .use_db(env.surrealdb_dbname) + .await?; + + let raw_users: Vec = db.select("app_users").await?; + let users: Vec = raw_users.into_iter().map(Into::into).collect(); + + for user in users { + println!("{:?}", user); + } + + Ok(()) +} diff --git a/src/utils/extract_email.rs b/src/utils/extract_email.rs index b84ff15..471bb83 100644 --- a/src/utils/extract_email.rs +++ b/src/utils/extract_email.rs @@ -2,10 +2,24 @@ use crate::decode_access_token; use axum::http::{header::AUTHORIZATION, HeaderMap}; pub fn extract_email(headers: &HeaderMap) -> Option { + println!("๐Ÿ“ฅ Received headers: {:?}", headers); + let auth_header = headers.get(AUTHORIZATION)?.to_str().ok()?; + println!("๐Ÿ” Authorization Header: {}", auth_header); + let token = auth_header.strip_prefix("Bearer ")?; - let token_data = decode_access_token(token).ok()?; - Some(token_data.claims.sub) + println!("๐Ÿงช Token: {}", token); + + match decode_access_token(token) { + Ok(data) => { + println!("โœ… Token claims: {:?}", data.claims); + Some(data.claims.sub) + } + Err(e) => { + eprintln!("โŒ Failed to decode token: {}", e); + None + } + } } pub fn extract_email_token(token: String) -> Option { diff --git a/src/utils/query_list.rs b/src/utils/query_list.rs index c3d1c2a..04345ab 100644 --- a/src/utils/query_list.rs +++ b/src/utils/query_list.rs @@ -1,15 +1,22 @@ use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto}; use anyhow::{bail, Result}; use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; +use surrealdb::sql::Thing; use surrealdb::{engine::remote::ws::Client, Surreal}; use super::bind_filter_value; +fn thing_to_string(thing: &Thing) -> String { + format!("{}", thing.id) +} + pub async fn query_list_with_meta( db: &Surreal, table: &str, meta: &MetaRequestDto, conditions: Vec, + custom_select: Option, ) -> Result>> where T: DeserializeOwned + Serialize, @@ -19,44 +26,58 @@ where if page < 1 || per_page < 1 { bail!("Invalid pagination: page and per_page must be greater than 0"); } - let start = (page - 1) * per_page; - let mut sql = format!("SELECT * FROM {}", table); - if !conditions.is_empty() { - sql.push_str(" WHERE "); - sql.push_str(&conditions.join(" AND ")); - } + // SELECT QUERY + let sql = custom_select.unwrap_or_else(|| { + let mut s = format!("SELECT * FROM {}", table); + if !conditions.is_empty() { + s.push_str(" WHERE "); + s.push_str(&conditions.join(" AND ")); + } + if let Some(sort_by) = &meta.sort_by { + let order = match meta + .order + .clone() + .unwrap_or_default() + .to_uppercase() + .as_str() + { + "DESC" => "DESC", + _ => "ASC", + }; + s.push_str(&format!(" ORDER BY {} {}", sort_by, order)); + } + s.push_str(" LIMIT $per_page START $start"); + s + }); - if let Some(sort_by) = &meta.sort_by { - let order = match meta - .order - .clone() - .unwrap_or_else(|| "ASC".into()) - .to_uppercase() - .as_str() - { - "ASC" => "ASC", - "DESC" => "DESC", - _ => "ASC", - }; - sql.push_str(&format!(" ORDER BY {} {}", sort_by, order)); - } - - sql.push_str(" LIMIT $per_page START $start"); - - let mut query = db.query(sql); + let mut query_exec = db.query(sql); if let Some(search) = &meta.search { if !search.is_empty() { - query = query.bind(("search", search.clone())); + query_exec = query_exec.bind(("search", search.clone())); } } if let Some(filter_val) = meta.filter.clone() { - query = bind_filter_value(query, filter_val); + query_exec = bind_filter_value(query_exec, filter_val); } - query = query.bind(("per_page", per_page)).bind(("start", start)); + query_exec = query_exec + .bind(("per_page", per_page)) + .bind(("start", start)); - let items: Vec = query.await?.take(0)?; + let raw: Vec = query_exec.await?.take(0)?; + + let mapped: Vec = raw + .into_iter() + .map(|mut item| { + if let Some(id) = item.get("id").cloned() { + if let Ok(thing) = serde_json::from_value::(id.clone()) { + item["id"] = Value::String(thing_to_string(&thing)); + } + } + serde_json::from_value(item).unwrap() + }) + .collect(); // COUNT QUERY let mut count_sql = format!("SELECT count() FROM {}", table); @@ -64,7 +85,6 @@ where count_sql.push_str(" WHERE "); count_sql.push_str(&conditions.join(" AND ")); } - let mut count_query = db.query(count_sql); if let Some(search) = &meta.search { if !search.is_empty() { @@ -74,7 +94,6 @@ where if let Some(filter_val) = meta.filter.clone() { count_query = bind_filter_value(count_query, filter_val); } - let count_result: Vec = count_query.await?.take(0)?; let total = count_result.first().map(|c| c.count); @@ -85,7 +104,7 @@ where }; Ok(ResponseListSuccessDto { - data: items, + data: mapped, meta: Some(meta), }) }