chore: normalize code

This commit is contained in:
Maulana Sodiqin
2025-05-17 02:08:59 +07:00
parent 874f25aa80
commit a43ef8ec04
104 changed files with 691 additions and 655 deletions
+27
View File
@@ -0,0 +1,27 @@
use axum::{
Router,
routing::{delete, get, post, put},
};
pub mod permissions_controller;
pub mod permissions_dto;
pub mod permissions_enum;
pub mod permissions_guard;
pub mod permissions_repository;
pub mod permissions_schema;
pub mod permissions_service;
pub use permissions_controller::*;
pub use permissions_dto::*;
pub use permissions_enum::*;
pub use permissions_guard::*;
pub use permissions_repository::*;
pub use permissions_schema::*;
pub fn permissions_router() -> Router {
Router::new()
.route("/", get(get_permission_list))
.route("/create", post(post_create_permission))
.route("/detail/{id}", get(get_permission_by_id))
.route("/update/{id}", put(put_update_permission))
.route("/delete/{id}", delete(delete_permission))
}
@@ -0,0 +1,166 @@
use axum::{
extract::{Path, Query},
response::IntoResponse,
Extension, Json,
};
use crate::{
v1::{
permissions_dto::{PermissionsItemDto, PermissionsRequestDto},
permissions_service::PermissionsService,
},
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
};
use super::{permissions_guard, PermissionsEnum};
#[utoipa::path(
get,
path = "/v1/permissions",
params(
("page" = Option<i64>, Query, description = "Page number"),
("per_page" = Option<i64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search keyword"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Order ASC or DESC"),
("filter" = Option<String>, Query, description = "Filter value"),
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Get permission list", body = ResponseListSuccessDto<Vec<PermissionsItemDto>>)
),
tag = "Permissions"
)]
pub async fn get_permission_list(
headers: axum::http::HeaderMap,
Extension(state): Extension<AppState>,
Query(meta): Query<MetaRequestDto>,
) -> impl IntoResponse {
match permissions_guard(
&headers,
state.clone(),
vec![PermissionsEnum::ReadListPermissions],
)
.await
{
Ok(_) => PermissionsService::get_permission_list(&state, meta).await,
Err(response) => response,
}
}
#[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<PermissionsItemDto>)
),
tag = "Permissions"
)]
pub async fn get_permission_by_id(
headers: axum::http::HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match permissions_guard(
&headers,
state.clone(),
vec![PermissionsEnum::ReadDetailPermissions],
)
.await
{
Ok(_) => PermissionsService::get_permission_by_id(&state, id).await,
Err(response) => response,
}
}
#[utoipa::path(
post,
security(
("Bearer" = [])
),
path = "/v1/permissions/create",
request_body = PermissionsRequestDto,
responses(
(status = 201, description = "Create new permission", body = MessageResponseDto)
),
tag = "Permissions"
)]
pub async fn post_create_permission(
headers: axum::http::HeaderMap,
Extension(state): Extension<AppState>,
Json(payload): Json<PermissionsRequestDto>,
) -> impl IntoResponse {
match permissions_guard(
&headers,
state.clone(),
vec![PermissionsEnum::CreatePermissions],
)
.await
{
Ok(_) => PermissionsService::create_role(&state, payload).await,
Err(response) => response,
}
}
#[utoipa::path(
put,
security(
("Bearer" = [])
),
path = "/v1/permissions/update/{id}",
request_body = PermissionsRequestDto,
responses(
(status = 200, description = "Update permission", body = MessageResponseDto)
),
tag = "Permissions"
)]
pub async fn put_update_permission(
headers: axum::http::HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<PermissionsRequestDto>,
) -> impl IntoResponse {
match permissions_guard(
&headers,
state.clone(),
vec![PermissionsEnum::UpdatePermissions],
)
.await
{
Ok(_) => PermissionsService::update_permission(&state, payload, id).await,
Err(response) => response,
}
}
#[utoipa::path(
delete,
security(
("Bearer" = [])
),
path = "/v1/permissions/delete/{id}",
responses(
(status = 200, description = "Delete permission", body = MessageResponseDto)
),
tag = "Permissions"
)]
pub async fn delete_permission(
headers: axum::http::HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match permissions_guard(
&headers,
state.clone(),
vec![PermissionsEnum::DeletePermissions],
)
.await
{
Ok(_) => PermissionsService::delete_permission(&state, id).await,
Err(response) => response,
}
}
@@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct PermissionsRequestDto {
#[validate(length(min = 1, message = "Permission name must not be empty"))]
pub name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct PermissionsItemDto {
pub id: String,
pub name: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PermissionsQueryDto {
pub id: Thing,
pub name: String,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
@@ -0,0 +1,43 @@
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionsEnum {
ReadListUsers,
ReadDetailUsers,
CreateUsers,
DeleteUsers,
UpdateUsers,
ReadListRoles,
ReadDetailRoles,
CreateRoles,
DeleteRoles,
UpdateRoles,
ReadListPermissions,
ReadDetailPermissions,
CreatePermissions,
DeletePermissions,
UpdatePermissions,
}
impl fmt::Display for PermissionsEnum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let permission_str = match self {
PermissionsEnum::ReadListUsers => "Read List Users",
PermissionsEnum::ReadDetailUsers => "Read Detail Users",
PermissionsEnum::CreateUsers => "Create Users",
PermissionsEnum::DeleteUsers => "Delete Users",
PermissionsEnum::UpdateUsers => "Update Users",
PermissionsEnum::ReadListRoles => "Read List Roles",
PermissionsEnum::ReadDetailRoles => "Read Detail Roles",
PermissionsEnum::CreateRoles => "Create Roles",
PermissionsEnum::DeleteRoles => "Delete Roles",
PermissionsEnum::UpdateRoles => "Update Roles",
PermissionsEnum::ReadListPermissions => "Read List Permissions",
PermissionsEnum::ReadDetailPermissions => "Read Detail Permissions",
PermissionsEnum::CreatePermissions => "Create Permissions",
PermissionsEnum::DeletePermissions => "Delete Permissions",
PermissionsEnum::UpdatePermissions => "Update Permissions",
};
write!(f, "{}", permission_str)
}
}
@@ -0,0 +1,42 @@
use super::PermissionsEnum;
use crate::{common_response, extract_email, AppState, AuthRepository};
use axum::{
http::{HeaderMap, StatusCode},
response::Response,
};
pub async fn permissions_guard(
headers: &HeaderMap,
state: AppState,
required_permissions: Vec<PermissionsEnum>,
) -> Result<(), Response> {
let auth_repo = AuthRepository::new(&state);
let email = extract_email(headers).ok_or_else(|| {
common_response(
StatusCode::UNAUTHORIZED,
"Invalid or missing authorization token",
)
})?;
let raw_user = auth_repo
.query_get_stored_user(email.clone())
.await
.map_err(|_| {
common_response(
StatusCode::UNAUTHORIZED,
"User session expired or not found",
)
})?;
let role = raw_user.role;
let role_permissions: Vec<String> =
role.permissions.into_iter().map(|perm| perm.name).collect();
let has_all_permissions = required_permissions
.iter()
.all(|required| role_permissions.contains(&required.to_string()));
if !has_all_permissions {
return Err(common_response(
StatusCode::FORBIDDEN,
"You don't have the required permissions",
));
}
Ok(())
}
@@ -0,0 +1,159 @@
use super::{PermissionsItemDto, PermissionsItemDtoRaw, PermissionsSchema};
use crate::{
get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto, ResourceEnum,
ResponseListSuccessDto,
};
use anyhow::{bail, Result};
use imphnen_utils::extract_id;
pub struct PermissionsRepository<'a> {
state: &'a AppState,
}
impl<'a> PermissionsRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
pub async fn query_permission_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<PermissionsItemDto>>> {
let mut conditions = vec!["is_deleted = false".into()];
if meta.search.is_some() {
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));
}
let raw_result: ResponseListSuccessDto<Vec<PermissionsItemDtoRaw>> = query_list_with_meta(
&self.state.surrealdb_ws,
&ResourceEnum::Permissions.to_string(),
&meta,
conditions,
None,
)
.await?;
let transformed_data = raw_result
.data
.into_iter()
.map(|permission| PermissionsItemDto {
id: extract_id(&permission.id),
name: permission.name,
created_at: permission.created_at,
updated_at: permission.updated_at,
})
.collect();
Ok(ResponseListSuccessDto {
data: transformed_data,
meta: raw_result.meta,
})
}
pub async fn query_permission_by_id(
&self,
id: String,
) -> Result<PermissionsSchema> {
let db = &self.state.surrealdb_ws;
let result: Option<PermissionsSchema> = db
.select((ResourceEnum::Permissions.to_string(), id.clone()))
.await?;
match result {
Some(permission) if !permission.is_deleted => Ok(permission),
_ => bail!("Permission not found"),
}
}
pub async fn transformed_query_permission_by_id(
&self,
id: String,
) -> Result<PermissionsItemDto> {
let raw_result = self
.query_permission_by_id(id.clone())
.await?;
let transformed_data = PermissionsItemDto {
id: extract_id(&raw_result.id),
name: raw_result.name,
created_at: raw_result.created_at,
updated_at: raw_result.updated_at,
};
Ok(transformed_data)
}
pub async fn query_permission_by_name(
&self,
name: String,
) -> Result<PermissionsSchema> {
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT * FROM {} WHERE name = $name AND is_deleted = false",
ResourceEnum::Permissions.to_string()
);
let result: Vec<PermissionsSchema> =
db.query(sql).bind(("name", name.clone())).await?.take(0)?;
if let Some(permission) = result.into_iter().next() {
Ok(permission.into())
} else {
bail!("Permission not found")
}
}
pub async fn query_create_permission(
&self,
data: PermissionsSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record: Option<PermissionsSchema> = db
.create(ResourceEnum::Permissions.to_string())
.content(data)
.await?;
match record {
Some(_) => Ok("Success create permission".into()),
None => bail!("Failed to create permission"),
}
}
pub async fn query_update_permission(
&self,
data: PermissionsSchema,
) -> Result<String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?;
let existing = self.query_permission_by_id(data.id.id.to_raw()).await?;
if existing.is_deleted {
bail!("Permission already deleted");
}
let merged = PermissionsSchema {
created_at: existing.created_at,
..data.clone()
};
let record: Option<PermissionsSchema> =
db.update(record_key).merge(merged).await?;
match record {
Some(_) => Ok("Success update permission".into()),
None => bail!("Failed to update permission"),
}
}
pub async fn query_delete_permission(&self, id: String) -> Result<String> {
let db = &self.state.surrealdb_ws;
let permission_id = make_thing(&ResourceEnum::Permissions.to_string(), &id);
let permission = self
.query_permission_by_id(permission_id.id.to_raw())
.await?;
if permission.is_deleted {
bail!("Permission already deleted");
}
let record_key = get_id(&permission.id)?;
let record: Option<PermissionsSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
.await?;
match record {
Some(_) => Ok("Success delete permission".into()),
None => bail!("Failed to delete permission"),
}
}
}
@@ -0,0 +1,27 @@
use crate::{make_thing, ResourceEnum};
use serde::{Deserialize, Serialize};
use surrealdb::{sql::Thing, Uuid};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PermissionsSchema {
pub id: Thing,
pub name: String,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
impl Default for PermissionsSchema {
fn default() -> Self {
PermissionsSchema {
id: make_thing(
&ResourceEnum::Permissions.to_string(),
&Uuid::new_v4().to_string(),
),
name: String::new(),
is_deleted: false,
created_at: None,
updated_at: None,
}
}
}
@@ -0,0 +1,112 @@
use crate::{
common_response, make_thing, success_list_response, success_response,
validate_request, AppState, MetaRequestDto, PermissionsRepository,
PermissionsSchema, ResourceEnum, ResponseListSuccessDto, ResponseSuccessDto,
};
use axum::http::StatusCode;
use axum::response::Response;
use super::PermissionsRequestDto;
pub struct PermissionsService;
impl PermissionsService {
pub async fn get_permission_list(
state: &AppState,
meta: MetaRequestDto,
) -> Response {
let repo = PermissionsRepository::new(state);
match repo.query_permission_list(meta).await {
Ok(data) => {
let response = ResponseListSuccessDto {
data: data.data,
meta: data.meta,
};
success_list_response(response)
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
pub async fn get_permission_by_id(state: &AppState, id: String) -> Response {
let repo = PermissionsRepository::new(state);
match repo.transformed_query_permission_by_id(id).await {
Ok(permission) => success_response(ResponseSuccessDto { data: permission }),
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
}
}
pub async fn create_role(
state: &AppState,
payload: PermissionsRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&payload) {
return common_response(status, &message);
}
let repo = PermissionsRepository::new(state);
match repo.query_permission_by_name(payload.name.clone()).await {
Ok(_role) => {
return common_response(
StatusCode::CONFLICT,
"Permission name already exists",
);
}
Err(err) if err.to_string().contains("not found") => {}
Err(e) => {
return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string());
}
}
match repo
.query_create_permission(PermissionsSchema {
name: payload.name,
..Default::default()
})
.await
{
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn update_permission(
state: &AppState,
payload: PermissionsRequestDto,
id: String,
) -> Response {
if let Err((status, message)) = validate_request(&payload) {
return common_response(status, &message);
}
let repo = PermissionsRepository::new(state);
match repo
.query_update_permission(PermissionsSchema {
id: make_thing(&ResourceEnum::Permissions.to_string(), &id),
name: payload.name,
..Default::default()
})
.await
{
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => {
if e.to_string().contains("not found") {
common_response(StatusCode::NOT_FOUND, "Permission not found")
} else {
common_response(StatusCode::BAD_REQUEST, &e.to_string())
}
}
}
}
pub async fn delete_permission(state: &AppState, id: String) -> Response {
let repo = PermissionsRepository::new(state);
match repo.query_delete_permission(id).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => {
if e.to_string().contains("not found") {
common_response(StatusCode::NOT_FOUND, "Permission not found")
} else {
common_response(StatusCode::BAD_REQUEST, &e.to_string())
}
}
}
}
}