feat: Enhance user permissions and update event handling with improved response structures

This commit is contained in:
MythEclipse
2025-08-15 00:32:15 +07:00
parent 01e20baa45
commit 8402415649
11 changed files with 147 additions and 43 deletions
@@ -65,6 +65,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
(
"3b9f8c4e-6a2d-4f8a-9a12-2d6f8b3c4e5a",
vec![
PermissionsEnum::ReadListUsers, // Added ReadListUsers permission
PermissionsEnum::ReadOwnMentorProfile,
PermissionsEnum::UpdateOwnMentorProfile,
PermissionsEnum::ReadOwnMentorStatus,
@@ -1,7 +1,7 @@
use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date, make_thing};
use std::time::Instant;
use tracing::instrument;
use tracing::info;
@@ -46,8 +46,17 @@ impl<'a> EventsRepository<'a> {
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
// Attempt to parse the ID. If it's a full Thing (e.g., "events:some_id"), extract the ID part.
// Otherwise, assume it's already the raw ID.
let parsed_id = if id.contains(":") {
let thing = make_thing(ResourceEnum::Events.to_string().as_str(), &id);
get_id(&thing)?.1.to_string()
} else {
id.clone()
};
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
.with_id(&id)
.with_id(&parsed_id)
.with_select_fields(vec!["*"]);
let sql = builder.build();
info!(query = %sql, "Executing SurrealDB query");
@@ -82,7 +82,7 @@ impl<'a> TestimonialsRepository<'a> {
pub async fn query_create_testimonial(
&self,
data: TestimonialsSchema,
) -> Result<String> {
) -> Result<TestimonialsSchema> { // Change return type from String to TestimonialsSchema
let now = Instant::now();
let db = &self.state.surrealdb_ws;
info!(
@@ -102,7 +102,7 @@ impl<'a> TestimonialsRepository<'a> {
}
match record {
Some(_) => Ok("Success create testimonial".into()),
Some(created_testimonial) => Ok(created_testimonial), // Return the created testimonial
None => bail!("Failed to create testimonial"),
}
}
@@ -11,7 +11,7 @@ use imphnen_libs::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
};
use imphnen_utils::{
common_response, success_list_response, success_response, validate_request,
common_response, success_list_response, success_response, success_created_response, validate_request,
};
pub struct TestimonialsService;
@@ -46,8 +46,8 @@ impl TestimonialsService {
Ok(testimonial) if !testimonial.is_deleted => {
success_response(ResponseSuccessDto {
data: TestimonialsDetailItemDto {
id: testimonial.id.id.to_raw(),
user_id: testimonial.user.id.id.to_raw(),
id: testimonial.id.to_raw(),
user_id: testimonial.user.id.to_raw(),
user_fullname: testimonial.user.fullname,
role: testimonial.role,
content: testimonial.content,
@@ -72,7 +72,19 @@ impl TestimonialsService {
let repo = TestimonialsRepository::new(state);
let schema = TestimonialsSchema::create(payload, &authenticated_user.id);
match repo.query_create_testimonial(schema).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Ok(created_testimonial) => {
success_created_response(ResponseSuccessDto {
data: TestimonialsDetailItemDto {
id: created_testimonial.id.to_raw(),
user_id: created_testimonial.user.id.to_raw(),
user_fullname: authenticated_user.fullname.clone(),
role: created_testimonial.role,
content: created_testimonial.content,
created_at: created_testimonial.created_at,
updated_at: created_testimonial.updated_at,
},
})
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
@@ -368,6 +368,7 @@ mod tests {
let auth_request = AuthRequest {
code: "4/0-ARAA6EeEKN8rlQ_Dh5XAAA_dCpKFwKa3-Jl9cO7I".to_string(),
state: "valid_state".to_string(),
redirect_uri: None,
};
let result = auth_request.validate();
@@ -379,6 +380,7 @@ mod tests {
let auth_request = AuthRequest {
code: "authorization/code/with/slashes".to_string(),
state: "valid_state".to_string(),
redirect_uri: None,
};
let result = auth_request.validate();
@@ -390,6 +392,7 @@ mod tests {
let auth_request = AuthRequest {
code: "authorization+code+with+plus".to_string(),
state: "valid_state".to_string(),
redirect_uri: None,
};
let result = auth_request.validate();
@@ -401,6 +404,7 @@ mod tests {
let auth_request = AuthRequest {
code: "authorization=code=with=equals=".to_string(),
state: "valid_state".to_string(),
redirect_uri: None,
};
let result = auth_request.validate();
@@ -412,6 +416,7 @@ mod tests {
let auth_request = AuthRequest {
code: "authorization@code#with$invalid%chars".to_string(),
state: "valid_state".to_string(),
redirect_uri: None,
};
let result = auth_request.validate();
@@ -423,6 +428,7 @@ mod tests {
let auth_request = AuthRequest {
code: "".to_string(),
state: "valid_state".to_string(),
redirect_uri: None,
};
let result = auth_request.validate();
@@ -441,6 +447,7 @@ mod tests {
let auth_request = AuthRequest {
code: "test_code".to_string(),
state: token,
redirect_uri: None,
};
// Validate and extract PKCE verifier
@@ -459,6 +466,7 @@ mod tests {
let auth_request = AuthRequest {
code: "test_code".to_string(),
state: token,
redirect_uri: None,
};
// Legacy validation should still work
+22 -16
View File
@@ -1,6 +1,7 @@
use super::{
UsersActiveInactiveRequestDto, UsersCreateRequestDto,
UsersSetNewPasswordRequestDto, UsersUpdateRequestDto,
users_dto::UsersDetailQueryDto, // Add this line
};
use crate::{
AppState, MetaRequestDto, ResponseListSuccessDto, UsersRepository, UsersSchema,
@@ -31,22 +32,27 @@ pub trait UsersServiceTrait: Send + Sync + 'static {
async fn update_user_password(state: &AppState, email: String, payload: UsersSetNewPasswordRequestDto) -> Response;
async fn get_user_by_mentor_id(state: &AppState, mentor_id: String) -> Response;
async fn delete_user(state: &AppState, id: String) -> Response;
async fn get_user_by_email(&self, email: &str, state: &AppState) -> Result<Option<UserDto>>;
async fn create_user_by_dto(&self, new_user: CreateUserDto, state: &AppState) -> Result<UserDto>;
async fn update_user_avatar(&self, email: &str, avatar_url: Option<String>, state: &AppState) -> Result<()>;
async fn upload_file(state: &AppState, user_id: String, multipart: Multipart) -> Response;
}
#[derive(Clone)]
pub struct UsersService;
impl UsersService {
}
#[async_trait]
impl UsersServiceTrait for UsersService {
async fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Response {
async fn get_user_by_id_internal(&self, id: &surrealdb::sql::Thing, state: &AppState) -> Result<UsersDetailQueryDto>;
async fn get_user_by_email(&self, email: &str, state: &AppState) -> Result<Option<UserDto>>;
async fn create_user_by_dto(&self, new_user: CreateUserDto, state: &AppState) -> Result<UserDto>;
async fn update_user_avatar(&self, email: &str, avatar_url: Option<String>, state: &AppState) -> Result<()>;
async fn upload_file(state: &AppState, user_id: String, multipart: Multipart) -> Response;
}
#[derive(Clone)]
pub struct UsersService;
impl UsersService {
}
#[async_trait]
impl UsersServiceTrait for UsersService {
async fn get_user_by_id_internal(&self, id: &surrealdb::sql::Thing, state: &AppState) -> Result<UsersDetailQueryDto> {
let repo = crate::UsersRepository::new(state);
repo.query_user_by_id(id).await
}
async fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = UsersRepository::new(state);
match repo.query_user_list(meta).await {
Ok(data) => {
+13 -1
View File
@@ -6,6 +6,9 @@ use imphnen_libs::{AppState, jsonwebtoken::decode_access_token};
use imphnen_utils::common_response;
use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt};
use std::convert::Infallible;
use imphnen_iam::v1::users::{users_service::{UsersService, UsersServiceTrait}};
use imphnen_libs::ResourceEnum;
use imphnen_utils::make_thing;
pub async fn auth_middleware(
Extension(_state): Extension<AppState>, // state is currently unused in this middleware
@@ -32,6 +35,15 @@ pub async fn auth_middleware(
)),
};
req.extensions_mut().insert(claims);
let user_id = claims.user_id.clone();
let repo = UsersService {};
let thing_id = make_thing(&ResourceEnum::Users.to_string(), &user_id);
let user_data = match repo.get_user_by_id_internal(&thing_id, &_state).await {
Ok(user) => user,
Err(_) => return Ok(common_response(StatusCode::UNAUTHORIZED, "User not found")),
};
req.extensions_mut().insert(user_data);
Ok(next.run(req).await)
}
+11
View File
@@ -43,3 +43,14 @@ pub fn common_response(status: StatusCode, message: &str) -> Response {
)
.into_response()
}
pub fn success_created_response<T: Serialize>(params: ResponseSuccessDto<T>) -> Response {
(
StatusCode::CREATED,
Json(json!({
"data": params.data,
"version": "0.1.0",
})),
)
.into_response()
}
+42 -15
View File
@@ -21,8 +21,10 @@ SKIP_COMPREHENSIVE=false
SKIP_CRUD=false
GENERATE_REPORT=false
VERBOSE=false
SKIP_CLEAR=false
SKIP_SEED=false
while getopts "sbcrgvh" opt; do
while getopts "sbcrgvhkd" opt; do
case ${opt} in
s ) START_SERVER=true ;;
b ) SKIP_BASIC=true ;;
@@ -30,7 +32,7 @@ while getopts "sbcrgvh" opt; do
r ) SKIP_CRUD=true ;;
g ) GENERATE_REPORT=true ;;
v ) VERBOSE=true ;;
h )
h )
echo "IMPHNEN API Test Suite"
echo "Usage: $0 [OPTIONS]"
echo ""
@@ -42,6 +44,8 @@ while getopts "sbcrgvh" opt; do
echo " -g Generate JSON test report"
echo " -v Verbose output (show all INFO logs)"
echo " -h Show this help message"
echo " -d Skip database clear"
echo " -k Skip database seeding"
echo ""
echo "Examples:"
echo " $0 # Run all tests"
@@ -49,11 +53,18 @@ while getopts "sbcrgvh" opt; do
echo " $0 -g # Run tests and generate report"
echo " $0 -sv # Start server with verbose output"
echo " $0 -bc # Run only public endpoint tests"
echo " $0 -d # Skip database clear"
echo " $0 -k # Skip database seeding"
echo " $0 -dk # Skip database clear and seeding"
exit 0
;;
d ) SKIP_CLEAR=true ;;
k ) SKIP_SEED=true ;;
\? ) echo "Invalid option: -$OPTARG" >&2; echo "Use -h for help" >&2; exit 1 ;;
esac
done
# Shift past the options
shift "$((OPTIND-1))"
if ! command -v curl &> /dev/null; then
echo "Error: 'curl' tidak ditemukan. Mohon install terlebih dahulu." >&2
@@ -72,6 +83,7 @@ FALED_TESTS_SUMMARY=()
PASS_COUNT=0
FAIL_COUNT=0
TEST_TESTIMONIAL_ID=""
TEST_EVENT_ID=""
CYAN='\033[0;36m'
YELLOW='\033[0;33m'
@@ -162,6 +174,10 @@ test_server_connection() {
}
clear_database() {
if [ "$SKIP_CLEAR" = true ]; then
write_test_log "INFO" "Melewatkan pembersihan database."
return
fi
write_test_log "INFO" "Membersihkan database via WebSocket..."
if ! RUST_LOG=debug cargo run --bin clear_db_test --release; then
write_test_log "ERROR" "Gagal membersihkan database."
@@ -433,7 +449,7 @@ test_comprehensive_with_user() {
;;
"mentor@example.com")
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
@@ -448,7 +464,7 @@ test_comprehensive_with_user() {
;;
"user@example.com")
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 403 "" true
test_api_endpoint "Get Users List - $fullname" "GET" "/v1/users" 200 "" true
test_api_endpoint "Get Roles List - $fullname" "GET" "/v1/roles" 403 "" true
test_api_endpoint "Get Permissions List - $fullname" "GET" "/v1/permissions" 403 "" true
test_api_endpoint "Get Mentors List - $fullname" "GET" "/v1/mentors" 200 "" true
@@ -471,7 +487,7 @@ test_comprehensive_with_user() {
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
;;
*)
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 403 "" true
test_api_endpoint "Users with Sort - $fullname" "GET" "/v1/users?sort_by=created_at&order=DESC" 200 "" true
;;
esac
@@ -522,7 +538,7 @@ test_authentication_endpoints() {
local forgot_password_data
forgot_password_data=$(jq -n --arg email "$TEST_EMAIL" '{email: $email}')
test_api_endpoint "Forgot Password Test" "POST" "/v1/auth/forgot" 400 "$forgot_password_data"
test_api_endpoint "Forgot Password Test" "POST" "/v1/auth/forgot" 200 "$forgot_password_data"
local new_password_data
new_password_data=$(jq -n --arg token "some_reset_token" --arg pass "newpassword123!A" '{token: $token, password: $pass}')
@@ -602,9 +618,9 @@ test_crud_operations() {
local testimonial_data
testimonial_data=$(jq -n --arg content "Test testimonial via Bash $(date +%s)" '{role: "Student", content: $content}')
local testimonial_response=$(test_api_endpoint "Create Testimonial" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true)
local testimonial_response
testimonial_response=$(test_api_endpoint "Create Testimonial" "POST" "/v1/cms/landing/testimonials/create" 201 "$testimonial_data" true)
TEST_TESTIMONIAL_ID=$(echo "$testimonial_response" | jq -r '.data.id // empty')
write_test_log "INFO" "TEST_TESTIMONIAL_ID: $TEST_TESTIMONIAL_ID"
write_test_log "INFO" "Captured Testimonial ID: $TEST_TESTIMONIAL_ID"
sleep 0.2
@@ -627,7 +643,10 @@ test_crud_operations() {
end_date: "2025-12-01T16:00:00Z",
location: null
}')
test_api_endpoint "Create Event" "POST" "/v1/cms/landing/events/create" 201 "$event_data" true
local event_response
event_response=$(test_api_endpoint "Create Event" "POST" "/v1/cms/landing/events/create" 201 "$event_data" true)
TEST_EVENT_ID=$(echo "$event_response" | jq -r '.data.id // empty')
write_test_log "INFO" "Captured Event ID: $TEST_EVENT_ID"
}
test_roles_and_permissions() {
@@ -687,8 +706,12 @@ test_events_endpoints() {
test_api_endpoint "Get Events with Pagination" "GET" "/v1/cms/landing/events?page=1&per_page=5" 200
test_api_endpoint "Get Events with Search" "GET" "/v1/cms/landing/events?search=tech" 200
local test_event_id="e1a2b3c4-5d6e-7f8g-9h0i-1j2k3l4m5n6o"
test_api_endpoint "Get Event By ID" "GET" "/v1/cms/landing/events/detail/$test_event_id" 200
# Ensure TEST_EVENT_ID is not empty before testing
if [ -n "$TEST_EVENT_ID" ]; then
test_api_endpoint "Get Event By ID" "GET" "/v1/cms/landing/events/detail/$TEST_EVENT_ID" 200
else
write_test_log "WARN" "✗ Get Event By ID - Dilewati: TEST_EVENT_ID tidak tersedia"
fi
}
test_testimonials_endpoints() {
@@ -803,11 +826,15 @@ fi
clear_database
printf "\n${CYAN}=== Menjalankan Seeders ===${NC}\n"
if ! RUST_LOG=debug cargo run --bin seeder; then
write_test_log "ERROR" "Gagal menjalankan seeder roles permissions."
exit 1
if [ "$SKIP_SEED" = true ]; then
write_test_log "INFO" "Melewatkan seeding database."
else
if ! RUST_LOG=debug cargo run --bin seeder; then
write_test_log "ERROR" "Gagal menjalankan seeder roles permissions."
exit 1
fi
write_test_log "SUCCESS" "Seeders selesai."
fi
write_test_log "SUCCESS" "Seeders selesai."
printf "\n${CYAN}=== Menampilkan User yang Tersedia ===${NC}\n"
+7 -1
View File
@@ -57,13 +57,19 @@ mod auth_login_tests {
gender: None,
birthdate: None,
domicile: None,
identity_document_url: None,
bio: None,
last_education: None,
linkedin_url: None,
github_url: None,
cv_url: None,
portfolio_url: None,
website_url: None,
twitter_url: None,
location: None,
skills: None,
experience: None,
education: None,
career_status: None,
role: crate::make_thing("app_roles", &role.id),
mentor_id: None,
created_at: imphnen_utils::get_iso_date(),
+14 -2
View File
@@ -31,13 +31,19 @@ mod auth_repository_test {
gender: None,
birthdate: None,
domicile: None,
identity_document_url: None,
bio: None,
last_education: None,
linkedin_url: None,
github_url: None,
cv_url: None,
portfolio_url: None,
website_url: None,
twitter_url: None,
location: None,
skills: None,
experience: None,
education: None,
career_status: None,
role: make_thing("app_roles", &get_role_id(state).await),
mentor_id: None,
created_at: get_iso_date(),
@@ -90,13 +96,19 @@ mod auth_repository_test {
gender: None,
birthdate: None,
domicile: None,
identity_document_url: None,
bio: None,
last_education: None,
linkedin_url: None,
github_url: None,
cv_url: None,
portfolio_url: None,
website_url: None,
twitter_url: None,
location: None,
skills: None,
experience: None,
education: None,
career_status: None,
role: RolesDetailQueryDto {
id: make_thing("app_roles", &Uuid::new_v4().to_string()),
name: "Dummy Role".into(),