diff --git a/imphnen-iam/src/v1/permissions/permissions_dto.rs b/imphnen-iam/src/v1/permissions/permissions_dto.rs index c589e21..19aaf3b 100644 --- a/imphnen-iam/src/v1/permissions/permissions_dto.rs +++ b/imphnen-iam/src/v1/permissions/permissions_dto.rs @@ -17,6 +17,17 @@ pub struct PermissionsItemDto { pub updated_at: Option, } +impl PermissionsItemDto { + pub fn from(dto: &PermissionsQueryDto) -> Self { + Self { + id: dto.id.id.to_raw(), + name: dto.name.clone(), + created_at: dto.created_at.clone(), + updated_at: dto.updated_at.clone(), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PermissionsQueryDto { pub id: Thing, diff --git a/imphnen-iam/src/v1/permissions/permissions_enum.rs b/imphnen-iam/src/v1/permissions/permissions_enum.rs index f544e0e..aa244fe 100644 --- a/imphnen-iam/src/v1/permissions/permissions_enum.rs +++ b/imphnen-iam/src/v1/permissions/permissions_enum.rs @@ -7,6 +7,7 @@ pub enum PermissionsEnum { CreateUsers, DeleteUsers, UpdateUsers, + ActivateUsers, ReadListRoles, ReadDetailRoles, CreateRoles, @@ -27,6 +28,7 @@ impl fmt::Display for PermissionsEnum { PermissionsEnum::CreateUsers => "Create Users", PermissionsEnum::DeleteUsers => "Delete Users", PermissionsEnum::UpdateUsers => "Update Users", + PermissionsEnum::ActivateUsers => "Activate Users", PermissionsEnum::ReadListRoles => "Read List Roles", PermissionsEnum::ReadDetailRoles => "Read Detail Roles", PermissionsEnum::CreateRoles => "Create Roles", diff --git a/imphnen-iam/src/v1/roles/roles_controller.rs b/imphnen-iam/src/v1/roles/roles_controller.rs index ecb8151..e9333eb 100644 --- a/imphnen-iam/src/v1/roles/roles_controller.rs +++ b/imphnen-iam/src/v1/roles/roles_controller.rs @@ -1,13 +1,16 @@ use axum::{ + Extension, Json, extract::{Path, Query}, response::IntoResponse, - Extension, Json, }; -use super::{RolesItemDto, RolesRequestCreateDto, RolesRequestUpdateDto}; +use super::{ + RolesDetailItemDto, RolesListItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, +}; use crate::{ - permissions_guard, v1::roles_service::RolesService, AppState, MessageResponseDto, - MetaRequestDto, PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto, + AppState, MessageResponseDto, MetaRequestDto, PermissionsEnum, + ResponseListSuccessDto, ResponseSuccessDto, permissions_guard, + v1::roles_service::RolesService, }; #[utoipa::path( @@ -26,7 +29,7 @@ use crate::{ ("filter_by" = Option, Query, description = "Field to filter by"), ), responses( - (status = 200, description = "Get role list", body = ResponseListSuccessDto>) + (status = 200, description = "Get role list", body = ResponseListSuccessDto>) ), tag = "Roles" )] @@ -55,7 +58,7 @@ pub async fn get_role_list( path = "/v1/roles/detail/{id}", params(("id" = String, Path, description = "Role ID")), responses( - (status = 200, description = "Get role by ID", body = ResponseSuccessDto) + (status = 200, description = "Get role by ID", body = ResponseSuccessDto) ), tag = "Roles" )] diff --git a/imphnen-iam/src/v1/roles/roles_dto.rs b/imphnen-iam/src/v1/roles/roles_dto.rs index 28dc98a..6cb541e 100644 --- a/imphnen-iam/src/v1/roles/roles_dto.rs +++ b/imphnen-iam/src/v1/roles/roles_dto.rs @@ -5,17 +5,25 @@ use utoipa::ToSchema; use validator::Validate; #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] -pub struct RolesRequestDto { +pub struct RolesRequestUpdateDto { + #[validate(length(min = 1, message = "Role name must not be empty"))] + pub name: Option, + pub permissions: Option>, + pub overwrite: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct RolesRequestCreateDto { #[validate(length(min = 1, message = "Role name must not be empty"))] pub name: String, - pub permissions: Option>, + pub permissions: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct RolesListItemDto { pub id: String, pub name: String, - pub permissions_count: u64, + pub permissions_count: usize, pub created_at: Option, pub updated_at: Option, } @@ -24,11 +32,29 @@ pub struct RolesListItemDto { pub struct RolesDetailItemDto { pub id: String, pub name: String, + pub is_deleted: bool, pub permissions: Vec, pub created_at: Option, pub updated_at: Option, } +impl RolesDetailItemDto { + pub fn from(dto: &RolesDetailQueryDto) -> Self { + Self { + id: dto.id.id.to_raw(), + name: dto.name.clone(), + is_deleted: dto.is_deleted, + permissions: dto + .permissions + .iter() + .map(PermissionsItemDto::from) + .collect(), + created_at: dto.created_at.clone(), + updated_at: dto.updated_at.clone(), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RolesDetailQueryDto { pub id: Thing, diff --git a/imphnen-iam/src/v1/roles/roles_enum.rs b/imphnen-iam/src/v1/roles/roles_enum.rs index d377243..202c22f 100644 --- a/imphnen-iam/src/v1/roles/roles_enum.rs +++ b/imphnen-iam/src/v1/roles/roles_enum.rs @@ -4,8 +4,7 @@ use std::fmt; pub enum RolesEnum { Admin, User, - Student, - Staf, + Staff, } impl fmt::Display for RolesEnum { @@ -13,8 +12,7 @@ impl fmt::Display for RolesEnum { let roles_str = match self { RolesEnum::Admin => "Admin", RolesEnum::User => "User", - RolesEnum::Student => "Student", - RolesEnum::Staf => "Staf", + RolesEnum::Staff => "Staff", }; write!(f, "{}", roles_str) } diff --git a/imphnen-iam/src/v1/roles/roles_repository.rs b/imphnen-iam/src/v1/roles/roles_repository.rs index 46ebc50..fcae8da 100644 --- a/imphnen-iam/src/v1/roles/roles_repository.rs +++ b/imphnen-iam/src/v1/roles/roles_repository.rs @@ -1,13 +1,15 @@ use super::{ - RolesItemByIdDto, RolesItemByIdDtoRaw, RolesItemDto, RolesItemDtoRaw, RolesRequestCreateDto, RolesRequestUpdateDto, RolesSchema + RolesDetailItemDto, RolesDetailQueryDto, RolesListItemDto, RolesRequestCreateDto, + RolesRequestUpdateDto, RolesSchema, }; use crate::{ - extract_id, get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto, - PermissionsItemDto, ResourceEnum, ResponseListSuccessDto, + AppState, MetaRequestDto, PermissionsItemDto, ResourceEnum, + ResponseListSuccessDto, extract_id, get_id, make_thing, query_list_with_meta, }; -use anyhow::{bail, Result}; -use surrealdb::sql::Thing; +use anyhow::{Result, bail}; +use imphnen_utils::DetailQueryBuilder; use surrealdb::Uuid; +use surrealdb::sql::Thing; pub struct RolesRepository<'a> { state: &'a AppState, @@ -18,74 +20,73 @@ impl<'a> RolesRepository<'a> { Self { state } } - pub async fn query_raw_role_by_id(&self, id: &str) -> Result { - let db = &self.state.surrealdb_ws; - let role: Option = - db.select((ResourceEnum::Roles.to_string(), id)).await?; - match role { - Some(r) if !r.is_deleted => Ok(r), - _ => bail!("Role not found"), - } - } - pub async fn query_role_list( &self, meta: MetaRequestDto, - ) -> Result>> { + ) -> Result>> { let mut conditions = vec!["is_deleted = false".into()]; - if meta.search.is_some() { - conditions.push("string::contains(name, $search)".into()); + if let Some(_search) = meta.search.as_deref().filter(|s| !s.is_empty()) { + conditions.push("string::contains(name ?? '', $search)".into()); } - if meta.filter_by.is_some() && meta.filter.is_some() { - let filter_by = meta.filter_by.as_ref().unwrap(); - conditions.push(format!("{} = $filter", filter_by)); + if let (Some(filter_by), Some(filter_val)) = + (meta.filter_by.as_ref(), meta.filter.as_ref()) + { + if !filter_val.is_empty() { + conditions.push(format!("{} = $filter", filter_by)); + } } - let raw_result: ResponseListSuccessDto> = query_list_with_meta( - &self.state.surrealdb_ws, - &ResourceEnum::Roles.to_string(), - &meta, - conditions, - None, - ) - .await?; - let transformed_data = raw_result + let raw_result: ResponseListSuccessDto> = + query_list_with_meta( + &self.state.surrealdb_ws, + &ResourceEnum::Roles.to_string(), + &meta, + conditions, + None, + "name", + None, + ) + .await?; + let data = raw_result .data .into_iter() - .map(|role| { - RolesItemDto { - name: role.name, - created_at: role.created_at, - updated_at: role.updated_at, - permissions: role.permissions - .into_iter() - .map(|perm| PermissionsItemDto { - id: extract_id(&perm.id), - name: perm.name, - created_at: perm.created_at, - updated_at: perm.updated_at, - }) - .collect::>(), - id: extract_id(&role.id), - - } + .map(|role| RolesListItemDto { + id: extract_id(&role.id), + name: role.name, + created_at: role.created_at, + updated_at: role.updated_at, + permissions_count: role.permissions.len(), }) - .collect::>(); - let transformed_meta = raw_result.meta; + .collect(); Ok(ResponseListSuccessDto { - data: transformed_data, - meta: transformed_meta, + data, + meta: raw_result.meta, }) } - pub async fn query_role_by_name(&self, name: String) -> Result { + pub async fn query_role_by_name( + &self, + name: String, + ) -> Result { let db = &self.state.surrealdb_ws; - let sql = format!( - "SELECT *, permissions FROM {} WHERE name = $name AND is_deleted = false LIMIT 1 FETCH permissions", - ResourceEnum::Roles.to_string() - ); - let mut result = db.query(sql).bind(("name", name.clone())).await?; - let role: Option = result.take(0)?; - let role = match role { + let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string()) + .with_where("name") + .where_value(name.clone()) + .with_select_fields(vec![ + "id", + "name", + "permissions", + "created_at", + "updated_at", + "is_deleted", + ]) + .with_fetch("permissions"); + + let sql = builder.build(); + let result: Option = builder + .apply_bindings(db.query(sql).bind(("name", name))) + .await? + .take(0)?; + let role = match result { Some(r) if !r.is_deleted => r, _ => bail!("Role not found"), }; @@ -98,8 +99,8 @@ impl<'a> RolesRepository<'a> { created_at: perm.created_at, updated_at: perm.updated_at, }) - .collect::>(); - Ok(RolesItemByIdDto { + .collect(); + Ok(RolesDetailItemDto { id: extract_id(&role.id), name: role.name, is_deleted: role.is_deleted, @@ -109,16 +110,23 @@ impl<'a> RolesRepository<'a> { }) } - pub async fn query_role_by_id(&self, id: String) -> Result { + pub async fn query_role_by_id(&self, id: String) -> Result { let db = &self.state.surrealdb_ws; - let query = format!( - "SELECT *, permissions.* AS permissions - FROM app_roles:⟨{}⟩ WHERE is_deleted = false FETCH permissions", - id - ); - let mut result = db.query(query).await?; - let role: Option = result.take(0)?; - let role = match role { + let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string()) + .with_id(&id) + .with_select_fields(vec![ + "id", + "name", + "is_deleted", + "permissions", + "created_at", + "updated_at", + ]) + .with_fetch("permissions"); + let sql = builder.build(); + let result: Option = + builder.apply_bindings(db.query(sql)).await?.take(0)?; + let role = match result { Some(r) if !r.is_deleted => r, _ => bail!("Role not found"), }; @@ -131,8 +139,8 @@ impl<'a> RolesRepository<'a> { created_at: perm.created_at, updated_at: perm.updated_at, }) - .collect::>(); - Ok(RolesItemByIdDto { + .collect(); + Ok(RolesDetailItemDto { id: extract_id(&role.id), name: role.name, is_deleted: role.is_deleted, @@ -147,14 +155,12 @@ impl<'a> RolesRepository<'a> { payload: RolesRequestCreateDto, ) -> Result { let db = &self.state.surrealdb_ws; - let role_id = Uuid::new_v4().to_string(); let permission_things: Vec = payload .permissions .iter() .map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id)) .collect(); - let role = RolesSchema { id: make_thing(&ResourceEnum::Roles.to_string(), &role_id), name: payload.name, @@ -163,12 +169,10 @@ impl<'a> RolesRepository<'a> { created_at: Some(crate::get_iso_date()), updated_at: Some(crate::get_iso_date()), }; - let _: Option = db .create((&ResourceEnum::Roles.to_string(), role_id)) .content(role) .await?; - Ok("Role with permissions created successfully".into()) } @@ -178,31 +182,11 @@ impl<'a> RolesRepository<'a> { data: RolesRequestUpdateDto, ) -> Result { let db = &self.state.surrealdb_ws; - let thing_id = make_thing(&ResourceEnum::Roles.to_string(), &id); - let existing = self.query_raw_role_by_id(&id).await?; + let existing = self.query_role_by_id(id.clone()).await?; if existing.is_deleted { bail!("Role already deleted"); } - let permissions: Vec = if let Some(permission_ids) = &data.permissions { - permission_ids - .iter() - .map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id)) - .collect() - } else { - existing - .permissions - .iter() - .map(|p| make_thing(&ResourceEnum::Permissions.to_string(), &p.id.to_raw())) - .collect() - }; - let merged = RolesSchema { - id: thing_id, - name: data.name.unwrap_or(existing.name), - permissions, - is_deleted: existing.is_deleted, - created_at: existing.created_at, - updated_at: Some(crate::get_iso_date()), - }; + let merged = RolesSchema::update(data, id.clone(), existing); let record: Option = db.update(get_id(&merged.id)?).content(merged).await?; match record { diff --git a/imphnen-iam/src/v1/roles/roles_schema.rs b/imphnen-iam/src/v1/roles/roles_schema.rs index d5ae49d..4898257 100644 --- a/imphnen-iam/src/v1/roles/roles_schema.rs +++ b/imphnen-iam/src/v1/roles/roles_schema.rs @@ -1,7 +1,12 @@ +use super::{ + RolesDetailItemDto, RolesDetailQueryDto, RolesRequestCreateDto, + RolesRequestUpdateDto, +}; +use crate::{ResourceEnum, make_thing}; +use imphnen_utils::get_iso_date; use serde::{Deserialize, Serialize}; -use surrealdb::{sql::Thing, Uuid}; - -use crate::{make_thing, ResourceEnum}; +use std::collections::HashSet; +use surrealdb::{Uuid, sql::Thing}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RolesSchema { @@ -31,3 +36,80 @@ impl Default for RolesSchema { } } } + +impl RolesSchema { + pub fn from(dto: RolesDetailQueryDto) -> Self { + Self { + id: dto.id, + name: dto.name, + permissions: dto + .permissions + .into_iter() + .map(|perm| { + make_thing(&ResourceEnum::Permissions.to_string(), &perm.id.to_raw()) + }) + .collect(), + is_deleted: dto.is_deleted, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } + + pub fn create(dto: RolesRequestCreateDto) -> Self { + let permissions: Vec = dto + .permissions + .into_iter() + .map(|id| make_thing(&ResourceEnum::Permissions.to_string(), &id)) + .collect(); + Self { + id: make_thing( + &ResourceEnum::Roles.to_string(), + &Uuid::new_v4().to_string(), + ), + name: dto.name, + permissions, + is_deleted: false, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), + } + } + + pub fn update( + dto: RolesRequestUpdateDto, + id: String, + existing: RolesDetailItemDto, + ) -> Self { + let name = dto.name.unwrap_or(existing.name); + let permissions: Vec = + match (dto.permissions, dto.overwrite.unwrap_or(false)) { + (Some(new_ids), true) => new_ids + .iter() + .map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id)) + .collect(), + (Some(new_ids), false) => { + let mut all_ids: HashSet = + existing.permissions.iter().map(|p| p.id.clone()).collect(); + for id in new_ids { + all_ids.insert(id); + } + all_ids + .into_iter() + .map(|id| make_thing(&ResourceEnum::Permissions.to_string(), &id)) + .collect() + } + (None, _) => existing + .permissions + .iter() + .map(|p| make_thing(&ResourceEnum::Permissions.to_string(), &p.id)) + .collect(), + }; + Self { + id: make_thing(&ResourceEnum::Roles.to_string(), &id), + name, + permissions, + is_deleted: existing.is_deleted, + created_at: existing.created_at, + updated_at: Some(get_iso_date()), + } + } +} diff --git a/imphnen-iam/src/v1/roles/roles_service.rs b/imphnen-iam/src/v1/roles/roles_service.rs index f381d7a..b21df84 100644 --- a/imphnen-iam/src/v1/roles/roles_service.rs +++ b/imphnen-iam/src/v1/roles/roles_service.rs @@ -1,7 +1,7 @@ use super::{RolesRepository, RolesRequestCreateDto, RolesRequestUpdateDto}; use crate::{ - common_response, success_list_response, success_response, validate_request, AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, + common_response, success_list_response, success_response, validate_request, }; use axum::{http::StatusCode, response::Response}; diff --git a/imphnen-iam/src/v1/users/users_controller.rs b/imphnen-iam/src/v1/users/users_controller.rs index 51784ea..442a78c 100644 --- a/imphnen-iam/src/v1/users/users_controller.rs +++ b/imphnen-iam/src/v1/users/users_controller.rs @@ -1,16 +1,16 @@ +use crate::{AppState, MetaRequestDto, v1::users_service::UsersService}; +use crate::{ + MessageResponseDto, PermissionsEnum, ResponseListSuccessDto, ResponseSuccessDto, + UsersCreateRequestDto, UsersDetailItemDto, permissions_guard, +}; use axum::extract::{Path, Query}; use axum::http::HeaderMap; use axum::response::IntoResponse; use axum::{Extension, Json}; -use crate::{ - permissions_guard, MessageResponseDto, PermissionsEnum, ResponseListSuccessDto, - ResponseSuccessDto, UsersActiveInactiveRequestDto, UsersCreateRequestDto, - UsersDetailItemDto, +use super::{ + UsersActiveInactiveRequestDto, UsersListItemDto, UsersUpdateRequestDto, }; -use crate::{v1::users_service::UsersService, AppState, MetaRequestDto}; - -use super::{UsersListItemDto, UsersUpdateRequestDto}; #[utoipa::path( get, @@ -204,7 +204,7 @@ pub async fn patch_user_active_status( match permissions_guard( &headers, state.clone(), - vec![PermissionsEnum::UpdateUsers], + vec![PermissionsEnum::ActivateUsers], ) .await { diff --git a/imphnen-iam/src/v1/users/users_dto.rs b/imphnen-iam/src/v1/users/users_dto.rs index 2c71866..41c29f6 100644 --- a/imphnen-iam/src/v1/users/users_dto.rs +++ b/imphnen-iam/src/v1/users/users_dto.rs @@ -1,4 +1,4 @@ -use crate::{RolesDetailQueryDto, RolesItemDto}; +use crate::{RolesDetailItemDto, RolesDetailQueryDto}; use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -10,6 +10,17 @@ lazy_static! { static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap(); } +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct UsersActiveInactiveRequestDto { + pub is_active: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct UsersSetNewPasswordRequestDto { + pub password: String, + pub old_password: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct UsersCreateRequestDto { #[validate( @@ -71,7 +82,7 @@ pub struct UsersUpdateRequestDto { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct UsersDetailItemDto { pub id: String, - pub role: RolesItemDto, + pub role: RolesDetailItemDto, pub fullname: String, pub email: String, pub avatar: Option, @@ -83,6 +94,24 @@ pub struct UsersDetailItemDto { pub updated_at: String, } +impl UsersDetailItemDto { + pub fn from(dto: UsersDetailQueryDto) -> Self { + Self { + id: dto.id.id.to_raw(), + role: RolesDetailItemDto::from(&dto.role), + fullname: dto.fullname, + email: dto.email, + avatar: dto.avatar, + phone_number: dto.phone_number, + is_active: dto.is_active, + gender: dto.gender, + birthdate: dto.birthdate, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct UsersListItemDto { pub id: String, @@ -110,7 +139,7 @@ pub struct UsersListQueryDto { } impl UsersListQueryDto { - pub fn list_from(&self, role: String) -> UsersListItemDto { + pub fn from(&self, role: String) -> UsersListItemDto { UsersListItemDto { id: self.id.id.to_raw(), role, diff --git a/imphnen-iam/src/v1/users/users_repository.rs b/imphnen-iam/src/v1/users/users_repository.rs index 68e87ad..feab5df 100644 --- a/imphnen-iam/src/v1/users/users_repository.rs +++ b/imphnen-iam/src/v1/users/users_repository.rs @@ -1,27 +1,30 @@ use super::{UsersDetailQueryDto, UsersListItemDto, UsersListQueryDto, UsersSchema}; use crate::{ - AppState, MetaRequestDto, PermissionsItemDto, PermissionsItemDtoRaw, ResourceEnum, - ResponseListSuccessDto, RolesDetailQueryDto, extract_id, get_id, make_thing, + AppState, MetaRequestDto, PermissionsQueryDto, ResourceEnum, + ResponseListSuccessDto, RolesDetailQueryDto, get_id, make_thing, query_list_with_meta, }; use anyhow::{Result, bail}; +use imphnen_utils::DetailQueryBuilder; +use surrealdb::{Surreal, engine::remote::ws::Client}; pub struct UsersRepository<'a> { state: &'a AppState, } -pub fn build_user_by_field_query(field: &str) -> String { - format!( - r#" - SELECT *, role AS role - FROM {} - WHERE {} = $value AND is_deleted = false - LIMIT 1 - FETCH role, role.permissions - "#, - ResourceEnum::Users.to_string(), - field - ) +pub async fn update_partial_schema( + db: &Surreal, + table: &str, + id: &str, + patch: UsersSchema, +) -> Result { + let thing = make_thing(table, id); + let record_key = get_id(&thing)?; + let result: Option = db.update(record_key).merge(patch).await?; + match result { + Some(_) => Ok("Success update".into()), + None => bail!("Failed to update"), + } } impl<'a> UsersRepository<'a> { @@ -59,7 +62,7 @@ impl<'a> UsersRepository<'a> { .into_iter() .map(|schema| { let role = schema.clone().role.name; - schema.list_from(role) + schema.from(role) }) .collect::>(); @@ -75,13 +78,31 @@ impl<'a> UsersRepository<'a> { ) -> Result { let db = &self.state.surrealdb_ws; - let sql = build_user_by_field_query("email"); + let builder = DetailQueryBuilder::new(ResourceEnum::Users.to_string()) + .with_where("email") + .where_value(email.clone()) + .with_select_fields(vec![ + "id", + "fullname", + "email", + "avatar", + "phone_number", + "is_active", + "is_deleted", + "gender", + "birthdate", + "password", + "created_at", + "updated_at", + "role", + ]) + .with_fetch("role") + .with_fetch("role.permissions"); - let user_opt: Option = db - .query(sql) - .bind(("email", email.clone())) - .await? - .take(0)?; + let sql = builder.build(); + + let user_opt: Option = + builder.apply_bindings(db.query(sql)).await?.take(0)?; let Some(user) = user_opt else { bail!("User not found"); @@ -95,7 +116,7 @@ impl<'a> UsersRepository<'a> { .role .permissions .into_iter() - .map(|perm| PermissionsItemDtoRaw { + .map(|perm| PermissionsQueryDto { id: perm.id, name: perm.name, created_at: perm.created_at, @@ -130,15 +151,32 @@ impl<'a> UsersRepository<'a> { pub async fn query_user_by_id(&self, id: String) -> Result { let db = &self.state.surrealdb_ws; - let sql = build_user_by_field_query(&make_thing("app_users", &id).to_raw()); + let builder = DetailQueryBuilder::new(ResourceEnum::Users.to_string()) + .with_id(&id) + .with_select_fields(vec![ + "id", + "fullname", + "email", + "avatar", + "phone_number", + "is_active", + "is_deleted", + "gender", + "birthdate", + "password", + "created_at", + "updated_at", + "role", + ]) + .with_fetch("role") + .with_fetch("role.permissions"); - let user_opt: Option = db - .query(sql) - .bind(("email", make_thing("app_users", &id).to_raw())) - .await? - .take(0)?; + let sql = builder.build(); - let Some(user) = user_opt else { + let result: Option = + builder.apply_bindings(db.query(sql)).await?.take(0)?; + + let Some(user) = result else { bail!("User not found"); }; @@ -150,7 +188,7 @@ impl<'a> UsersRepository<'a> { .role .permissions .into_iter() - .map(|perm| PermissionsItemDtoRaw { + .map(|perm| PermissionsQueryDto { id: perm.id, name: perm.name, created_at: perm.created_at, @@ -201,10 +239,15 @@ impl<'a> UsersRepository<'a> { if existing.is_deleted { bail!("User already deleted"); } + let role_thing = if data.role == existing.role.id { + existing.role.id + } else { + data.clone().role + }; let merged = UsersSchema { password: existing.password, created_at: existing.created_at, - role: make_thing("app_roles", &existing.role.id), + role: role_thing, ..data.clone() }; let record: Option = db.update(record_key).merge(merged).await?; @@ -214,76 +257,13 @@ impl<'a> UsersRepository<'a> { } } - pub async fn query_active_inactive_user( - &self, - email: String, - data: UsersActiveInactiveSchema, - ) -> Result { + pub async fn query_delete_user(&self, id: String) -> Result { let db = &self.state.surrealdb_ws; - let user = self.query_user_by_email(email.clone()).await?; + let user = self.query_user_by_id(id).await?; if user.is_deleted { bail!("User already deleted"); } let record_key = get_id(&user.id)?; - let record: Option = db - .update(record_key) - .merge(UsersActiveInactiveSchema { - is_active: data.is_active, - }) - .await?; - match record { - Some(_) => Ok("Success update user".into()), - None => bail!("Failed to update user"), - } - } - - pub async fn query_active_inactive_user_by_id( - &self, - id: String, - data: UsersActiveInactiveSchema, - ) -> Result { - let db = &self.state.surrealdb_ws; - let record: Option = db - .update((ResourceEnum::Users.to_string(), id)) - .merge(UsersActiveInactiveSchema { - is_active: data.is_active, - }) - .await?; - 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: Option = db - .update((ResourceEnum::Users.to_string(), user.id.id.to_raw())) - .merge(UsersSetNewPasswordSchema { - password: data.password.clone(), - }) - .await?; - dbg!(record.clone()); - match record { - Some(_) => Ok("Success update password user".into()), - None => bail!("Failed to update password user"), - } - } - - pub async fn query_delete_user(&self, id: String) -> Result { - let db = &self.state.surrealdb_ws; - let user_id = make_thing(&ResourceEnum::Users.to_string(), &id); - let user = self.query_user_by_id(user_id.id.to_raw()).await?; - if user.is_deleted { - bail!("User already deleted"); - } - let id = make_thing(&ResourceEnum::Users.to_string(), &user.id); - let record_key = get_id(&id)?; let record: Option = db .update(record_key) .merge(serde_json::json!({ "is_deleted": true })) diff --git a/imphnen-iam/src/v1/users/users_schema.rs b/imphnen-iam/src/v1/users/users_schema.rs index 10b09aa..b80cd2e 100644 --- a/imphnen-iam/src/v1/users/users_schema.rs +++ b/imphnen-iam/src/v1/users/users_schema.rs @@ -1,8 +1,8 @@ -use super::{UsersDetailItemDto, UsersListItemDto}; -use crate::RolesItemDto; -use imphnen_utils::Crud; +use super::{UsersCreateRequestDto, UsersDetailQueryDto, UsersUpdateRequestDto}; +use imphnen_libs::{ResourceEnum, hash_password}; +use imphnen_utils::{get_iso_date, make_thing}; use serde::{Deserialize, Serialize}; -use surrealdb::sql::Thing; +use surrealdb::{Uuid, sql::Thing}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UsersSchema { @@ -21,46 +21,89 @@ pub struct UsersSchema { pub updated_at: String, } -impl Crud for UsersSchema { - fn list(&self, role: String) -> UsersListItemDto { - UsersListItemDto { - id: self.id.id.to_raw(), - role, - fullname: self.fullname.clone(), - email: self.email.clone(), - avatar: self.avatar.clone(), - phone_number: self.phone_number.clone(), - is_active: self.is_active, - created_at: self.created_at.clone(), - updated_at: self.updated_at.clone(), - } - } -} - -impl Crud for UsersSchema { - fn detail(&self, role: RolesItemDto) -> UsersDetailItemDto { - UsersDetailItemDto { - id: self.id.id.to_raw(), - role, - fullname: self.fullname.clone(), - email: self.email.clone(), - avatar: self.avatar.clone(), - phone_number: self.phone_number.clone(), - is_active: self.is_active, - gender: self.gender.clone(), - birthdate: self.birthdate.clone(), - created_at: self.created_at.clone(), - updated_at: self.updated_at.clone(), +impl Default for UsersSchema { + fn default() -> Self { + Self { + id: Thing::from(("app_users", "dummy")), + fullname: "".into(), + email: "".into(), + password: "".into(), + avatar: None, + phone_number: "".into(), + is_active: false, + is_deleted: false, + gender: None, + birthdate: None, + role: Thing::from(("app_roles", "dummy")), + created_at: "".into(), + updated_at: "".into(), } } } impl UsersSchema { - pub fn list_from(&self, role: String) -> UsersListItemDto { - self.list(role) + pub fn from(dto: UsersDetailQueryDto) -> Self { + Self { + id: dto.id, + fullname: dto.fullname, + email: dto.email, + avatar: dto.avatar, + phone_number: dto.phone_number, + is_active: dto.is_active, + is_deleted: dto.is_deleted, + gender: dto.gender, + birthdate: dto.birthdate, + password: dto.password, + created_at: dto.created_at, + updated_at: dto.updated_at, + role: make_thing(&ResourceEnum::Roles.to_string(), &dto.role.id.to_string()), + } } - pub fn detail_from(&self, role: RolesItemDto) -> UsersDetailItemDto { - self.detail(role) + pub fn update(user: UsersUpdateRequestDto, id: String) -> Self { + Self { + id: make_thing(&ResourceEnum::Users.to_string(), &id), + fullname: user.fullname, + email: user.email, + phone_number: user.phone_number, + is_active: user.is_active, + gender: user.gender, + birthdate: user.birthdate, + avatar: user.avatar, + is_deleted: false, + role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id), + updated_at: get_iso_date(), + ..Default::default() + } + } + + pub fn create(user: UsersCreateRequestDto) -> Self { + let password = hash_password(&user.password).unwrap(); + Self { + id: make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), + ), + fullname: user.fullname, + email: user.email, + password, + phone_number: user.phone_number, + is_active: false, + gender: None, + birthdate: None, + avatar: None, + is_deleted: false, + role: make_thing(&ResourceEnum::Roles.to_string(), &user.role_id), + created_at: get_iso_date(), + updated_at: get_iso_date(), + } + } + + pub fn patch_password(dto: UsersDetailQueryDto, password: String) -> Self { + Self { + password, + id: dto.id.clone(), + ..Self::from(dto) + } } } diff --git a/imphnen-iam/src/v1/users/users_service.rs b/imphnen-iam/src/v1/users/users_service.rs index 1009a96..f4f41f9 100644 --- a/imphnen-iam/src/v1/users/users_service.rs +++ b/imphnen-iam/src/v1/users/users_service.rs @@ -1,19 +1,17 @@ -use crate::{ - common_response, extract_email, get_iso_date, hash_password, make_thing, - success_list_response, success_response, validate_request, ResourceEnum, - ResponseSuccessDto, +use super::{ + UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersDetailItemDto, + UsersSetNewPasswordRequestDto, UsersUpdateRequestDto, }; use crate::{ - AppState, MetaRequestDto, ResponseListSuccessDto, UsersActiveInactiveSchema, - UsersRepository, UsersSchema, UsersSetNewPasswordSchema, + AppState, MetaRequestDto, ResponseListSuccessDto, UsersRepository, UsersSchema, +}; +use crate::{ + ResourceEnum, ResponseSuccessDto, common_response, extract_email, make_thing, + success_list_response, success_response, validate_request, }; use axum::http::HeaderMap; use axum::{http::StatusCode, response::Response}; - -use super::{ - UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersDetailItemDto, - UsersUpdateRequestDto, -}; +use imphnen_libs::{hash_password, verify_password}; pub struct UsersService; @@ -36,17 +34,7 @@ impl UsersService { let repo = UsersRepository::new(state); match repo.query_user_by_id(id).await { Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto { - data: UsersDetailItemDto { - id: user.id, - role: user.role, - fullname: user.fullname, - email: user.email, - avatar: user.avatar, - phone_number: user.phone_number, - is_active: user.is_active, - gender: user.gender, - birthdate: user.birthdate, - }, + data: UsersDetailItemDto::from(user), }), Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"), Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), @@ -55,22 +43,15 @@ impl UsersService { pub async fn get_user_me(headers: HeaderMap, state: &AppState) -> Response { let repo = UsersRepository::new(state); - let email = extract_email(&headers).unwrap(); - let user = repo.query_user_by_email(email).await.unwrap(); - match repo.query_user_by_id(user.id.id.to_raw()).await { - Ok(user) => success_response(ResponseSuccessDto { - data: UsersDetailItemDto { - id: user.id, - role: user.role, - fullname: user.fullname, - email: user.email, - avatar: user.avatar, - phone_number: user.phone_number, - is_active: user.is_active, - gender: user.gender, - birthdate: user.birthdate, - }, + let email = match extract_email(&headers) { + Some(email) => email, + None => return common_response(StatusCode::UNAUTHORIZED, "Invalid token"), + }; + match repo.query_user_by_email(email).await { + Ok(user) if !user.is_deleted => success_response(ResponseSuccessDto { + data: UsersDetailItemDto::from(user), }), + Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"), Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), } } @@ -90,19 +71,7 @@ impl UsersService { { return common_response(StatusCode::BAD_REQUEST, "User already exists"); } - let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &new_user.role_id); - match repo - .query_create_user(UsersSchema { - email: new_user.email.clone(), - fullname: new_user.fullname.clone(), - password: hash_password(&new_user.password).unwrap(), - phone_number: new_user.phone_number.clone(), - is_active: new_user.is_active.clone(), - role: role_thing, - ..Default::default() - }) - .await - { + match repo.query_create_user(UsersSchema::create(new_user)).await { Ok(msg) => common_response(StatusCode::CREATED, &msg), Err(err) => { common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) @@ -116,28 +85,10 @@ impl UsersService { user: UsersUpdateRequestDto, ) -> Response { let repo = UsersRepository::new(state); - if let Err((status, message)) = validate_request(&user) { return common_response(status, &message); } - - let user_id = make_thing(&ResourceEnum::Users.to_string(), &id); - let role_id = make_thing(&ResourceEnum::Roles.to_string(), ""); - - let updated_user = UsersSchema { - id: user_id, - fullname: user.fullname, - email: user.email, - phone_number: user.phone_number, - is_active: user.is_active, - gender: user.gender, - birthdate: user.birthdate, - avatar: user.avatar, - role: role_id, - updated_at: get_iso_date(), - ..Default::default() - }; - + let updated_user = UsersSchema::update(user, id); match repo.query_update_user(updated_user).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), @@ -150,30 +101,18 @@ impl UsersService { user: UsersUpdateRequestDto, ) -> Response { let repo = UsersRepository::new(state); - let email = extract_email(&headers).unwrap(); - let user_data = repo.query_user_by_email(email).await.unwrap(); + let email = match extract_email(&headers) { + Some(email) => email, + None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"), + }; + let user_data = match repo.query_user_by_email(email.clone()).await { + Ok(user) => user, + Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"), + }; if let Err((status, message)) = validate_request(&user) { return common_response(status, &message); } - let user_id = - make_thing(&ResourceEnum::Users.to_string(), &user_data.id.id.to_raw()); - let role_id = make_thing(&ResourceEnum::Roles.to_string(), ""); - let updated_user = UsersSchema { - id: user_id, - fullname: user.fullname, - email: user.email, - phone_number: user.phone_number, - - is_active: user.is_active, - - gender: user.gender, - birthdate: user.birthdate, - avatar: user.avatar, - - role: role_id, - updated_at: get_iso_date(), - ..Default::default() - }; + let updated_user = UsersSchema::update(user, user_data.id.id.to_raw()); match repo.query_update_user(updated_user).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), @@ -183,23 +122,23 @@ impl UsersService { pub async fn set_user_active_status( state: &AppState, id: String, - status: UsersActiveInactiveRequestDto, + payload: UsersActiveInactiveRequestDto, ) -> Response { let repo = UsersRepository::new(state); let thing_id = make_thing(&ResourceEnum::Users.to_string(), &id); match repo.query_user_by_id(thing_id.id.to_raw()).await { - Ok(_) => match repo - .query_active_inactive_user_by_id( - id, - UsersActiveInactiveSchema { - is_active: status.is_active, - }, - ) - .await - { - Ok(msg) => common_response(StatusCode::OK, &msg), - Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), - }, + Ok(user) if !user.is_deleted => { + let patch = UsersSchema { + id: user.id.clone(), + is_active: payload.is_active, + ..UsersSchema::from(user) + }; + match repo.query_update_user(patch).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } + Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"), Err(err) => common_response(StatusCode::BAD_REQUEST, &err.to_string()), } } @@ -207,10 +146,41 @@ impl UsersService { pub async fn update_user_password( state: &AppState, email: String, - new_password: UsersSetNewPasswordSchema, + payload: UsersSetNewPasswordRequestDto, ) -> Response { let repo = UsersRepository::new(state); - match repo.query_update_password_user(email, new_password).await { + let user = match repo.query_user_by_email(email.clone()).await { + Ok(user) if !user.is_deleted => user, + _ => return common_response(StatusCode::NOT_FOUND, "User not found"), + }; + let verify_result = match verify_password(&payload.old_password, &user.password) + { + Ok(result) => result, + Err(_) => { + return common_response( + StatusCode::BAD_REQUEST, + "Old password is incorrect", + ); + } + }; + if !verify_result { + return common_response(StatusCode::BAD_REQUEST, "Old password is incorrect"); + } + let new_password = match hash_password(&payload.password) { + Ok(pw) => pw, + Err(_) => { + return common_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to hash password", + ); + } + }; + let patch = UsersSchema { + id: user.id.clone(), + password: new_password, + ..Default::default() + }; + match repo.query_update_user(patch).await { Ok(msg) => common_response(StatusCode::OK, &msg), Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), } diff --git a/imphnen-utils/src/make_thing.rs b/imphnen-utils/src/make_thing.rs index 39986ab..735bf1a 100644 --- a/imphnen-utils/src/make_thing.rs +++ b/imphnen-utils/src/make_thing.rs @@ -3,3 +3,7 @@ use surrealdb::sql::Thing; pub fn make_thing(table: &str, id: &str) -> Thing { Thing::from((table, id)) } + +pub fn make_thing_str(table: &str, id: &str) -> String { + format!("{}:⟨{}⟩", table, id) +} diff --git a/imphnen-utils/src/query_builder.rs b/imphnen-utils/src/query_builder.rs index 33e1a3e..df6eab0 100644 --- a/imphnen-utils/src/query_builder.rs +++ b/imphnen-utils/src/query_builder.rs @@ -1,4 +1,7 @@ use imphnen_libs::MetaRequestDto; +use surrealdb::engine::remote::ws::Client; +use surrealdb::method::Query; +use surrealdb::sql::Thing; pub struct ListQueryBuilder { resource: String, @@ -135,3 +138,106 @@ impl ListQueryBuilder { ) } } + +pub struct DetailQueryBuilder { + resource: String, + id: Option, + thing: Option, + where_field: Option, + where_value: Option, + select_fields: Vec, + fetch_fields: Vec, +} + +impl DetailQueryBuilder { + pub fn new(resource: impl Into) -> Self { + Self { + resource: resource.into(), + id: None, + thing: None, + where_field: None, + where_value: None, + select_fields: vec![], + fetch_fields: vec![], + } + } + + pub fn with_id(mut self, id: impl Into) -> Self { + if self.where_field.is_some() || self.thing.is_some() { + panic!("Cannot use with_id() after with_where() or with_thing()"); + } + self.id = Some(id.into()); + self + } + + pub fn with_thing(mut self, thing: &Thing) -> Self { + if self.id.is_some() || self.where_field.is_some() { + panic!("Cannot use with_thing() after with_id() or with_where()"); + } + self.thing = Some(thing.to_string()); // app_users:uuid + self.resource = thing.tb.clone(); // update resource dari thing + self + } + + pub fn with_where(mut self, field: impl Into) -> Self { + if self.id.is_some() || self.thing.is_some() { + panic!("Cannot use with_where() after with_id() or with_thing()"); + } + self.where_field = Some(field.into()); + self + } + + pub fn where_value(mut self, value: impl Into) -> Self { + self.where_value = Some(value.into()); + self + } + + pub fn with_select_fields(mut self, fields: Vec<&str>) -> Self { + self.select_fields = fields.into_iter().map(String::from).collect(); + self + } + + pub fn with_fetch(mut self, field: impl Into) -> Self { + self.fetch_fields.push(field.into()); + self + } + + pub fn build(&self) -> String { + let select_clause = if self.select_fields.is_empty() { + "*".to_string() + } else { + self.select_fields.join(", ") + }; + + let fetch_clause = if self.fetch_fields.is_empty() { + String::new() + } else { + format!("FETCH {}", self.fetch_fields.join(", ")) + }; + + let from_clause = if let Some(thing) = &self.thing { + thing.to_string() + } else if let Some(id) = &self.id { + format!("{}:⟨{}⟩", self.resource, id) + } else if let (Some(field), Some(_)) = (&self.where_field, &self.where_value) { + format!("{} WHERE {} = $value", self.resource, field) + } else { + panic!( + "You must set one of with_id(), with_thing(), or with_where()+where_value()" + ); + }; + + format!( + "SELECT {} FROM {} {}", + select_clause, from_clause, fetch_clause + ) + } + + pub fn apply_bindings<'q>(&self, query: Query<'q, Client>) -> Query<'q, Client> { + if let (Some(_), Some(value)) = (&self.where_field, &self.where_value) { + query.bind(("value", value.clone())) + } else { + query + } + } +}