From 199d0c885e9af038611de3f649667f923770b72f Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 14 Aug 2025 22:46:42 +0700 Subject: [PATCH] feat: Add axum-extra dependency and implement typed headers feat: Define unique index on users table for email and fix minor syntax error refactor: Update permissions_guard to use claims from JWT and improve user retrieval refactor: Modify user-related service methods to accept user details directly fix: Update token generation functions to include user ID and permissions test: Update Google OAuth flow tests to reflect changes in token generation --- Cargo.lock | 48 ++++++++++ Cargo.toml | 1 + .../src/bin/seed_roles_permissions.rs | 5 +- .../src/v1/mentors/mentors_controller.rs | 54 +++++------ .../gacha_claims/gacha_claims_controller.rs | 12 +-- .../v1/gacha_items/gacha_items_controller.rs | 30 +++--- .../v1/gacha_rolls/gacha_rolls_controller.rs | 24 ++--- imphnen-iam/Cargo.toml | 1 + imphnen-iam/src/v1/auth/auth_service.rs | 74 +++++++++----- .../v1/auth/google/google_oauth_service.rs | 6 +- .../v1/permissions/permissions_controller.rs | 30 +++--- .../src/v1/permissions/permissions_guard.rs | 71 +++++++------- imphnen-iam/src/v1/roles/roles_controller.rs | 30 +++--- imphnen-iam/src/v1/users/users_controller.rs | 96 +++++++++---------- imphnen-iam/src/v1/users/users_service.rs | 31 ++---- imphnen-libs/src/jsonwebtoken/mod.rs | 18 ++-- .../iam/auth/google/google_oauth_flow_test.rs | 8 +- 17 files changed, 305 insertions(+), 234 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce5065e..ebdb496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,6 +461,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-test" version = "17.3.0" @@ -1656,6 +1679,30 @@ dependencies = [ "foldhash", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http 1.3.1", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.3.1", +] + [[package]] name = "heapless" version = "0.8.0" @@ -2204,6 +2251,7 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-extra", "axum-test", "chrono", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 7b22074..dba2eba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ validator = { version = "0.20.0", features = ["derive"] } lazy_static = "1.4.0" regex = "1.11.1" axum-test = "17.2.0" +axum-extra = { version = "0.10.1", features = ["typed-header"] } fancy-regex = "0.15.0" futures = "0.3.31" tower = "0.5.2" diff --git a/imphnen-backend/src/bin/seed_roles_permissions.rs b/imphnen-backend/src/bin/seed_roles_permissions.rs index 32b8f71..5fceeea 100644 --- a/imphnen-backend/src/bin/seed_roles_permissions.rs +++ b/imphnen-backend/src/bin/seed_roles_permissions.rs @@ -15,6 +15,9 @@ async fn main() -> Result<(), Box> { db.use_ns(env.surrealdb_namespace.clone()) .use_db(env.surrealdb_dbname.clone()) .await?; + db.query("DEFINE INDEX user_email_index ON TABLE users COLUMNS email UNIQUE;") + .await?; + println!("✅ Index 'user_email_index' defined on table 'users' for column 'email'."); let roles_permissions = vec![ ( @@ -75,7 +78,7 @@ async fn main() -> Result<(), Box> { PermissionsEnum::ReadListGachaItems, PermissionsEnum::ReadDetailGachaItems, PermissionsEnum::ReadListUsers, - PermissionsEnum::ReadDetailUsers,, + PermissionsEnum::ReadDetailUsers, PermissionsEnum::CreateGachaClaims, PermissionsEnum::ReadDetailGachaClaims, PermissionsEnum::ReadDetailGachaRolls, diff --git a/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs b/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs index a3b83a4..1fdbe45 100644 --- a/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs +++ b/imphnen-dimentorin/src/v1/mentors/mentors_controller.rs @@ -57,13 +57,13 @@ pub async fn get_mentor_list( Query(meta): Query, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers, + Extension(app_state), vec![PermissionsEnum::ReadListMentors], ) .await { - Ok(_) => MentorsService::get_mentor_list(&app_state, meta).await, + Ok((_user, app_state)) => MentorsService::get_mentor_list(&app_state, meta).await, Err(response) => response, } } @@ -90,13 +90,13 @@ pub async fn get_mentor_by_id( Path(id): Path, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers, + Extension(app_state), vec![PermissionsEnum::ReadDetailMentors], ) .await { - Ok(_) => MentorsService::get_mentor_by_id(&app_state, &id).await, + Ok((_user, app_state)) => MentorsService::get_mentor_by_id(&app_state, &id).await, Err(response) => response, } } @@ -126,13 +126,13 @@ pub async fn put_update_mentor( Json(dto): Json, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers, + Extension(app_state), vec![PermissionsEnum::UpdateMentors], ) .await { - Ok(_) => MentorsService::update_mentor(&app_state, &id, dto).await, + Ok((_user, app_state)) => MentorsService::update_mentor(&app_state, &id, dto).await, Err(response) => response, } } @@ -159,13 +159,13 @@ pub async fn delete_mentor( Path(id): Path, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers, + Extension(app_state), vec![PermissionsEnum::DeleteMentors], ) .await { - Ok(_) => MentorsService::delete_mentor(&app_state, &id).await, + Ok((_user, app_state)) => MentorsService::delete_mentor(&app_state, &id).await, Err(response) => response, } } @@ -195,13 +195,13 @@ pub async fn put_verify_mentor( Json(dto): Json, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers, + Extension(app_state), vec![PermissionsEnum::VerifyMentors], ) .await { - Ok(_) => MentorsService::verify_mentor(&app_state, &id, dto).await, + Ok((_user, app_state)) => MentorsService::verify_mentor(&app_state, &id, dto).await, Err(response) => response, } } @@ -221,17 +221,17 @@ pub async fn put_verify_mentor( ) )] pub async fn get_mentor_me( - Extension(app_state): Extension, headers: HeaderMap, + Extension(app_state): Extension, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers.clone(), + Extension(app_state), vec![PermissionsEnum::ReadOwnMentorProfile], ) .await { - Ok(_) => { + Ok((_user, app_state)) => { let email = match extract_email(&headers) { Some(email) => email, None => { @@ -268,18 +268,18 @@ pub async fn get_mentor_me( ) )] pub async fn put_update_mentor_me( - Extension(app_state): Extension, headers: HeaderMap, + Extension(app_state): Extension, Json(dto): Json, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers.clone(), + Extension(app_state), vec![PermissionsEnum::UpdateOwnMentorProfile], ) .await { - Ok(_) => { + Ok((_user, app_state)) => { let email = match extract_email(&headers) { Some(email) => email, None => { @@ -325,17 +325,17 @@ pub async fn put_update_mentor_no_id() -> Response { ) )] pub async fn get_mentor_status( - Extension(app_state): Extension, headers: HeaderMap, + Extension(app_state): Extension, ) -> Response { match permissions_guard( - &headers, - app_state.clone(), + headers.clone(), + Extension(app_state), vec![PermissionsEnum::ReadOwnMentorStatus], ) .await { - Ok(_) => { + Ok((_user, app_state)) => { let email = match extract_email(&headers) { Some(email) => email, None => { diff --git a/imphnen-gacha/src/v1/gacha_claims/gacha_claims_controller.rs b/imphnen-gacha/src/v1/gacha_claims/gacha_claims_controller.rs index c250f85..554e93d 100644 --- a/imphnen-gacha/src/v1/gacha_claims/gacha_claims_controller.rs +++ b/imphnen-gacha/src/v1/gacha_claims/gacha_claims_controller.rs @@ -25,13 +25,13 @@ pub async fn get_detail_gacha_claim( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadDetailGachaClaims], ) .await { - Ok(_) => GachaClaimService::get_gacha_claim_by_id(&state, id).await, + Ok((_user, state)) => GachaClaimService::get_gacha_claim_by_id(&state, id).await, Err(response) => response, } } @@ -54,13 +54,13 @@ pub async fn post_create_gacha_claim( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::CreateGachaClaims], ) .await { - Ok(_) => GachaClaimService::create_gacha_claim(&state, payload).await, + Ok((_user, state)) => GachaClaimService::create_gacha_claim(&state, payload).await, Err(response) => response, } } diff --git a/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs b/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs index c8bbb64..ac13f0b 100644 --- a/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs +++ b/imphnen-gacha/src/v1/gacha_items/gacha_items_controller.rs @@ -36,13 +36,13 @@ pub async fn get_gacha_item_list( Query(meta): Query, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadListGachaItems], ) .await { - Ok(_) => GachaItemService::get_gacha_item_list(&state, meta).await, + Ok((_user, state)) => GachaItemService::get_gacha_item_list(&state, meta).await, Err(response) => response, } } @@ -65,13 +65,13 @@ pub async fn get_gacha_item_by_id( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadDetailGachaItems], ) .await { - Ok(_) => GachaItemService::get_gacha_item_by_id(&state, id).await, + Ok((_user, state)) => GachaItemService::get_gacha_item_by_id(&state, id).await, Err(response) => response, } } @@ -94,13 +94,13 @@ pub async fn post_create_gacha_item( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::CreateGachaItems], ) .await { - Ok(_) => GachaItemService::create_gacha_item(&state, payload).await, + Ok((_user, state)) => GachaItemService::create_gacha_item(&state, payload).await, Err(response) => response, } } @@ -124,13 +124,13 @@ pub async fn put_update_gacha_item( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::UpdateGachaItems], ) .await { - Ok(_) => GachaItemService::update_gacha_item(&state, payload, id).await, + Ok((_user, state)) => GachaItemService::update_gacha_item(&state, payload, id).await, Err(response) => response, } } @@ -152,13 +152,13 @@ pub async fn delete_gacha_item( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::DeleteGachaItems], ) .await { - Ok(_) => GachaItemService::delete_gacha_item(&state, id).await, + Ok((_user, state)) => GachaItemService::delete_gacha_item(&state, id).await, Err(response) => response, } } diff --git a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs index d98a4f9..ad509e6 100644 --- a/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs +++ b/imphnen-gacha/src/v1/gacha_rolls/gacha_rolls_controller.rs @@ -25,13 +25,13 @@ pub async fn get_detail_gacha_roll( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadDetailGachaRolls], ) .await { - Ok(_) => GachaRollService::get_gacha_roll_by_id(&state, id).await, + Ok((_user, state)) => GachaRollService::get_gacha_roll_by_id(&state, id).await, Err(response) => response, } } @@ -54,13 +54,13 @@ pub async fn post_create_gacha_roll( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::CreateGachaRolls], ) .await { - Ok(_) => GachaRollService::create_gacha_roll(&state, payload).await, + Ok((_user, state)) => GachaRollService::create_gacha_roll(&state, payload).await, Err(response) => response, } } @@ -81,13 +81,13 @@ pub async fn post_execute_gacha_roll( Extension(state): Extension, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers.clone(), + Extension(state), vec![PermissionsEnum::ExecuteGachaRolls], ) .await { - Ok(_) => GachaRollService::execute_roll_once(headers, &state).await, + Ok((_user, state)) => GachaRollService::execute_roll_once(headers, &state).await, Err(response) => response, } } @@ -110,13 +110,13 @@ pub async fn delete_gacha_roll( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::DeleteGachaRolls], ) .await { - Ok(_) => GachaRollService::soft_delete_gacha_roll(&state, id).await, + Ok((_user, state)) => GachaRollService::soft_delete_gacha_roll(&state, id).await, Err(response) => response, } } diff --git a/imphnen-iam/Cargo.toml b/imphnen-iam/Cargo.toml index d6f47cf..7ffdba2 100644 --- a/imphnen-iam/Cargo.toml +++ b/imphnen-iam/Cargo.toml @@ -31,6 +31,7 @@ log.workspace = true once_cell.workspace = true tracing.workspace = true uuid.workspace = true +axum-extra.workspace = true [dev-dependencies] dotenvy.workspace = true diff --git a/imphnen-iam/src/v1/auth/auth_service.rs b/imphnen-iam/src/v1/auth/auth_service.rs index 3c1a96a..9040ecd 100644 --- a/imphnen-iam/src/v1/auth/auth_service.rs +++ b/imphnen-iam/src/v1/auth/auth_service.rs @@ -9,7 +9,7 @@ use crate::{ decode_refresh_token, encode_access_token, encode_refresh_token, encode_reset_password_token, extract_email_token, generate_otp, get_iso_date, hash_password, make_thing, send_email, success_response, validate_request, - verify_password, + verify_password, surrealdb_init_ws, surrealdb_init_mem, }; use axum::{http::StatusCode, response::Response}; use surrealdb::Uuid; @@ -87,7 +87,8 @@ impl AuthServiceTrait for AuthService { ); } - let access_token = match encode_access_token(payload.email.clone()) { + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let access_token = match encode_access_token(payload.email.clone(), user.id.id.to_raw(), permissions) { Ok(token) => token, Err(_e) => { error!( @@ -101,7 +102,8 @@ impl AuthServiceTrait for AuthService { } }; - let refresh_token = match encode_refresh_token(payload.email.clone()) { + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let refresh_token = match encode_refresh_token(payload.email.clone(), user.id.id.to_raw(), permissions) { Ok(token) => token, Err(_e) => { error!( @@ -182,7 +184,8 @@ impl AuthServiceTrait for AuthService { ); } - let access_token = match encode_access_token(payload.email.clone()) { + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let access_token = match encode_access_token(payload.email.clone(), user.id.id.to_raw(), permissions) { Ok(token) => token, Err(_e) => { error!( @@ -196,7 +199,8 @@ impl AuthServiceTrait for AuthService { } }; - let refresh_token = match encode_refresh_token(payload.email.clone()) { + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let refresh_token = match encode_refresh_token(payload.email.clone(), user.id.id.to_raw(), permissions) { Ok(token) => token, Err(_e) => { error!( @@ -374,32 +378,54 @@ impl AuthServiceTrait for AuthService { } } - async fn mutation_refresh_token( - payload: AuthRefreshTokenRequestDto, - ) -> Response { + async fn mutation_refresh_token(payload: AuthRefreshTokenRequestDto) -> Response { if let Err((status, message)) = validate_request(&payload) { return common_response(status, &message); } - let email = match decode_refresh_token(&payload.refresh_token) { - Ok(token) => token.claims.sub, + + let surrealdb_ws = match surrealdb_init_ws().await { + Ok(db) => db, + Err(e) => { + error!("Failed to initialize websocket database: {}", e); + return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Database initialization error"); + } + }; + let surrealdb_mem = match surrealdb_init_mem().await { + Ok(db) => db, + Err(e) => { + error!("Failed to initialize memory database: {}", e); + return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Database initialization error"); + } + }; + let state = AppState { surrealdb_ws, surrealdb_mem }; + let user_repo = UsersRepository::new(&state); + let user = match decode_refresh_token(&payload.refresh_token) { + Ok(token_data) => { + match user_repo.query_user_by_email(token_data.claims.sub.clone()).await { + Ok(user) => user, + Err(_) => return common_response(StatusCode::UNAUTHORIZED, "User not found"), + } + }, Err(_e) => { return common_response(StatusCode::UNAUTHORIZED, "Invalid refresh token"); } }; - let access_token = match encode_access_token(email.clone()) { + + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); + let access_token = match encode_access_token(user.email.clone(), user.id.id.to_raw(), permissions.clone()) { Ok(token) => token, Err(_e) => { - error!("Failed to generate access token for {}: {}", email, _e); + error!("Failed to generate access token for {}: {}", user.email, _e); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate access token", ); } }; - let refresh_token = match encode_refresh_token(email.clone()) { + let refresh_token = match encode_refresh_token(user.email.clone(), user.id.id.to_raw(), permissions) { Ok(token) => token, Err(_e) => { - error!("Failed to generate refresh token for {}: {}", email, _e); + error!("Failed to generate refresh token for {}: {}", user.email, _e); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate refresh token", @@ -440,7 +466,8 @@ impl AuthServiceTrait for AuthService { ); } }; - let token = match encode_reset_password_token(user.email.clone()) { + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let token = match encode_reset_password_token(user.email.clone(), user.id.id.to_raw(), permissions) { Ok(token) => token, Err(_e) => { error!( @@ -525,32 +552,31 @@ impl AuthServiceTrait for AuthService { return common_response(status, &message); } let repo = UsersRepository::new(state); + let user_repo = UsersRepository::new(state); let email = match extract_email_token(payload.token.clone()) { - Some(token) => token, + Some(email) => email, None => { return common_response(StatusCode::BAD_REQUEST, "Invalid or missing token"); } }; + let user = match user_repo.query_user_by_email(email).await { + Ok(user) => user, + Err(_) => return common_response(StatusCode::BAD_REQUEST, "User not found"), + }; let password = match hash_password(&payload.password) { Ok(p) => p, Err(_e) => { - error!("Failed to hash new password for {}: {}", email, _e); + error!("Failed to hash new password for {}: {}", user.email, _e); return common_response( StatusCode::INTERNAL_SERVER_ERROR, "Failed to hash password", ); } }; - 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 patch = UsersSchema { id: user.id.clone(), password, - ..Default::default() + ..UsersSchema::from(user.clone()) }; match repo.query_update_user(patch).await { Ok(msg) => common_response(StatusCode::OK, &msg), diff --git a/imphnen-iam/src/v1/auth/google/google_oauth_service.rs b/imphnen-iam/src/v1/auth/google/google_oauth_service.rs index 66ce453..7f7978a 100644 --- a/imphnen-iam/src/v1/auth/google/google_oauth_service.rs +++ b/imphnen-iam/src/v1/auth/google/google_oauth_service.rs @@ -310,13 +310,15 @@ where } }; - let access_token = encode_access_token(user.email.clone()) + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let access_token = encode_access_token(user.email.clone(), user.id.clone(), permissions) .map_err(|e| { error!("Failed to generate access token for {}: {:?}", user.email, e); Error::Auth("Failed to generate access token".to_string()) })?; - let refresh_token = encode_refresh_token(user.email.clone()) + let permissions: Vec = user.role.permissions.iter().map(|p| p.name.clone()).collect(); +let refresh_token = encode_refresh_token(user.email.clone(), user.id.clone(), permissions) .map_err(|e| { error!("Failed to generate refresh token for {}: {:?}", user.email, e); Error::Auth("Failed to generate refresh token".to_string()) diff --git a/imphnen-iam/src/v1/permissions/permissions_controller.rs b/imphnen-iam/src/v1/permissions/permissions_controller.rs index b626421..2138ee0 100644 --- a/imphnen-iam/src/v1/permissions/permissions_controller.rs +++ b/imphnen-iam/src/v1/permissions/permissions_controller.rs @@ -41,13 +41,13 @@ pub async fn get_permission_list( Query(meta): Query, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadListPermissions], ) .await { - Ok(_) => PermissionsService::get_permission_list(&state, meta).await, + Ok((_user, state)) => PermissionsService::get_permission_list(&state, meta).await, Err(response) => response, } } @@ -70,13 +70,13 @@ pub async fn get_permission_by_id( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadDetailPermissions], ) .await { - Ok(_) => PermissionsService::get_permission_by_id(&state, id).await, + Ok((_user, state)) => PermissionsService::get_permission_by_id(&state, id).await, Err(response) => response, } } @@ -99,13 +99,13 @@ pub async fn post_create_permission( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::CreatePermissions], ) .await { - Ok(_) => PermissionsService::create_role(&state, payload).await, + Ok((_user, state)) => PermissionsService::create_role(&state, payload).await, Err(response) => response, } } @@ -129,13 +129,13 @@ pub async fn put_update_permission( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::UpdatePermissions], ) .await { - Ok(_) => PermissionsService::update_permission(&state, payload, id).await, + Ok((_user, state)) => PermissionsService::update_permission(&state, payload, id).await, Err(response) => response, } } @@ -157,13 +157,13 @@ pub async fn delete_permission( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::DeletePermissions], ) .await { - Ok(_) => PermissionsService::delete_permission(&state, id).await, + Ok((_user, state)) => PermissionsService::delete_permission(&state, id).await, Err(response) => response, } } diff --git a/imphnen-iam/src/v1/permissions/permissions_guard.rs b/imphnen-iam/src/v1/permissions/permissions_guard.rs index 4c5572f..52cd591 100644 --- a/imphnen-iam/src/v1/permissions/permissions_guard.rs +++ b/imphnen-iam/src/v1/permissions/permissions_guard.rs @@ -1,49 +1,41 @@ use super::PermissionsEnum; -use crate::{AppState, AuthRepository, common_response, extract_email, extract_email_async, UsersDetailQueryDto}; +use crate::{AppState, common_response, decode_access_token, UsersDetailQueryDto, UsersRepository}; use axum::{ http::{HeaderMap, StatusCode}, - response::Response, + response::Response, Extension, }; +use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt}; +use imphnen_utils::make_thing; pub async fn permissions_guard( - headers: &HeaderMap, - state: AppState, + headers: HeaderMap, + Extension(state): Extension, required_permissions: Vec, -) -> Result { - let auth_repo = AuthRepository::new(&state); - - // Try synchronous email extraction first (for internal JWT tokens) - let email = match extract_email(headers) { - Some(email) => email, - None => { - // If sync extraction fails, try async (for Google tokens) - match extract_email_async(headers).await { - Some(email) => email, - None => { - return Err(common_response( - StatusCode::UNAUTHORIZED, - "Invalid or missing authorization token", - )); - } - } - } - }; - - let raw_user = auth_repo - .query_get_stored_user(email.clone()) - .await +) -> Result<(UsersDetailQueryDto, AppState), Response> { + let auth_header = headers + .typed_get::>() + .ok_or_else(|| { + common_response( + StatusCode::UNAUTHORIZED, + "Invalid or missing authorization token", + ) + })?; + + let token = auth_header.token(); + + let claims = decode_access_token(token) .map_err(|_| { common_response( StatusCode::UNAUTHORIZED, - "User session expired or not found", + "Invalid or expired token", ) - })?; - let role_permissions: Vec = - raw_user.role.permissions.iter().map(|perm| perm.name.clone()).collect(); + })? + .claims; + // Use permissions from JWT for the check for required in &required_permissions { let required_str = required.to_string(); - if !role_permissions.contains(&required_str) { + if !claims.permissions.contains(&required_str) { eprintln!(" MISSING REQUIRED PERMISSION: {required_str}"); return Err(common_response( StatusCode::FORBIDDEN, @@ -51,5 +43,18 @@ pub async fn permissions_guard( )); } } - Ok(raw_user) + + // Fetch full user details from the database using user_id from JWT + let user_repo = UsersRepository::new(&state); + let user_id_thing = make_thing("app_users", &claims.user_id); + let raw_user = user_repo.query_user_by_id(&user_id_thing) + .await + .map_err(|_| { + common_response( + StatusCode::INTERNAL_SERVER_ERROR, // Changed to internal server error as user ID should be valid from JWT + "Failed to retrieve user details", + ) + })?; + + Ok((raw_user, state)) } diff --git a/imphnen-iam/src/v1/roles/roles_controller.rs b/imphnen-iam/src/v1/roles/roles_controller.rs index e9333eb..a59a1cc 100644 --- a/imphnen-iam/src/v1/roles/roles_controller.rs +++ b/imphnen-iam/src/v1/roles/roles_controller.rs @@ -39,13 +39,13 @@ pub async fn get_role_list( Query(meta): Query, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadListRoles], ) .await { - Ok(_) => RolesService::get_role_list(&state, meta).await, + Ok((_user, state)) => RolesService::get_role_list(&state, meta).await, Err(response) => response, } } @@ -68,13 +68,13 @@ pub async fn get_role_by_id( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadDetailRoles], ) .await { - Ok(_) => RolesService::get_role_by_id(&state, id).await, + Ok((_user, state)) => RolesService::get_role_by_id(&state, id).await, Err(response) => response, } } @@ -97,13 +97,13 @@ pub async fn post_create_role( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::CreateRoles], ) .await { - Ok(_) => RolesService::create_role(&state, payload).await, + Ok((_user, state)) => RolesService::create_role(&state, payload).await, Err(response) => response, } } @@ -127,13 +127,13 @@ pub async fn put_update_role( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::UpdateRoles], ) .await { - Ok(_) => RolesService::update_role(&state, id, payload).await, + Ok((_user, state)) => RolesService::update_role(&state, id, payload).await, Err(response) => response, } } @@ -155,13 +155,13 @@ pub async fn delete_role( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::DeleteRoles], ) .await { - Ok(_) => RolesService::delete_role(&state, id).await, + Ok((_user, state)) => RolesService::delete_role(&state, id).await, Err(response) => response, } } diff --git a/imphnen-iam/src/v1/users/users_controller.rs b/imphnen-iam/src/v1/users/users_controller.rs index 9876669..df8e834 100644 --- a/imphnen-iam/src/v1/users/users_controller.rs +++ b/imphnen-iam/src/v1/users/users_controller.rs @@ -49,30 +49,30 @@ pub async fn get_user_list( axum::extract::Query(meta): axum::extract::Query, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadListUsers], ) .await { - Ok(_) => UsersService::get_user_list(&state, meta).await, + Ok((_user, state)) => UsersService::get_user_list(&state, meta).await, Err(response) => response, } } #[utoipa::path( - get, - security( - ("Bearer" = []) - ), - path = "/v1/users/detail/{id}", - params( - ("id" = String, Path, description = "User ID") - ), - responses( - (status = 200, description = "Get user by ID", body = ResponseSuccessDto) - ), - tag = "Users" +get, +security( + ("Bearer" = []) + ), +path = "/v1/users/detail/{id}", +params( + ("id" = String, Path, description = "User ID") +), +responses( + (status = 200, description = "Get user by ID", body = ResponseSuccessDto) +), +tag = "Users" )] pub async fn get_user_by_id( headers: HeaderMap, @@ -80,34 +80,34 @@ pub async fn get_user_by_id( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ReadDetailUsers], ) .await { - Ok(_) => UsersService::get_user_by_id(&state, id).await, + Ok((_user, state)) => UsersService::get_user_by_id(&state, id).await, Err(response) => response, } } #[utoipa::path( - get, - security( - ("Bearer" = []) - ), - path = "/v1/users/me", - responses( - (status = 200, description = "Get user by ID", body = ResponseSuccessDto) - ), - tag = "Users" +get, +security( + ("Bearer" = []) + ), +path = "/v1/users/me", +responses( + (status = 200, description = "Get user by ID", body = ResponseSuccessDto) +), +tag = "Users" )] pub async fn get_user_me( - Extension(state): Extension, headers: HeaderMap, + Extension(state): Extension, ) -> impl IntoResponse { - match permissions_guard(&headers, state.clone(), vec![]).await { - Ok(_) => UsersService::get_user_me(headers, &state).await, + match permissions_guard(headers, Extension(state), vec![]).await { + Ok((user, state)) => UsersService::get_user_me(user, &state).await, Err(response) => response, } } @@ -130,13 +130,13 @@ pub async fn post_create_user( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::CreateUsers], ) .await { - Ok(_) => UsersService::create_user(&state, payload).await, + Ok((_user, state)) => UsersService::create_user(&state, payload).await, Err(response) => response, } } @@ -163,13 +163,13 @@ pub async fn put_update_user( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::UpdateUsers], ) .await { - Ok(_) => UsersService::update_user(&state, id, payload).await, + Ok((_user, state)) => UsersService::update_user(&state, id, payload).await, Err(response) => response, } } @@ -191,8 +191,8 @@ pub async fn put_update_user_me( Extension(state): Extension, Json(payload): Json, ) -> impl IntoResponse { - match permissions_guard(&headers, state.clone(), vec![]).await { - Ok(_) => UsersService::update_user_me(headers, &state, payload).await, + match permissions_guard(headers.clone(), Extension(state), vec![]).await { + Ok((_user, state)) => UsersService::update_user_me(headers, &state, payload).await, Err(response) => response, } } @@ -219,13 +219,13 @@ pub async fn patch_user_active_status( Json(payload): Json, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::ActivateUsers], ) .await { - Ok(_) => UsersService::set_user_active_status(&state, id, payload).await, + Ok((_user, state)) => UsersService::set_user_active_status(&state, id, payload).await, Err(response) => response, } } @@ -247,13 +247,13 @@ pub async fn delete_user( Path(id): Path, ) -> impl IntoResponse { match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![PermissionsEnum::DeleteUsers], ) .await { - Ok(_) => UsersService::delete_user(&state, id).await, + Ok((_user, state)) => UsersService::delete_user(&state, id).await, Err(response) => response, } } @@ -284,15 +284,15 @@ pub async fn upload_file( ) -> impl IntoResponse { // Check authentication first match permissions_guard( - &headers, - state.clone(), + headers, + Extension(state), vec![], // No specific permission needed, just authentication ) .await { - Ok(user) => { + Ok((user, state)) => { // Extract user ID from user data - let user_id = user.id.to_string(); + let user_id = user.id.id.to_string(); // Use user.id.id to get the actual ID // Process upload - don't use match here since it returns Response directly UsersService::upload_file(&state, user_id, multipart).await diff --git a/imphnen-iam/src/v1/users/users_service.rs b/imphnen-iam/src/v1/users/users_service.rs index 3dd2edf..8f44f44 100644 --- a/imphnen-iam/src/v1/users/users_service.rs +++ b/imphnen-iam/src/v1/users/users_service.rs @@ -7,7 +7,7 @@ use crate::{ }; use crate::{ ResponseSuccessDto, common_response, extract_email, extract_email_async, success_list_response, - success_response, validate_request, + success_response, validate_request, UsersDetailQueryDto, }; use axum::http::HeaderMap; use axum::{http::StatusCode, response::Response, extract::Multipart}; @@ -24,7 +24,7 @@ use serde_json::json; pub trait UsersServiceTrait: Send + Sync + 'static { async fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Response; async fn get_user_by_id(state: &AppState, id: String) -> Response; - async fn get_user_me(headers: HeaderMap, state: &AppState) -> Response; + async fn get_user_me(user: UsersDetailQueryDto, state: &AppState) -> Response; async fn create_user(state: &AppState, new_user: UsersCreateRequestDto) -> Response; async fn update_user(state: &AppState, id: String, user: UsersUpdateRequestDto) -> Response; async fn update_user_me(headers: HeaderMap, state: &AppState, user: UsersUpdateRequestDto) -> Response; @@ -76,28 +76,11 @@ impl UsersServiceTrait for UsersService { } } - async fn get_user_me(headers: HeaderMap, state: &AppState) -> Response { - let repo = UsersRepository::new(state); - - // Try synchronous email extraction first (for internal JWT tokens) - let email = match extract_email(&headers) { - Some(email) => email, - None => { - // If sync extraction fails, try async (for Google tokens) - match extract_email_async(&headers).await { - 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: UserDto::from(&user), // Corrected to use UserDto::from by reference - }), - Ok(_) => common_response(StatusCode::NOT_FOUND, "User not found"), - Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), - } + async fn get_user_me(user: UsersDetailQueryDto, _state: &AppState) -> Response { + // User data is already provided by the permissions_guard + success_response(ResponseSuccessDto { + data: UserDto::from(&user), + }) } async fn create_user( diff --git a/imphnen-libs/src/jsonwebtoken/mod.rs b/imphnen-libs/src/jsonwebtoken/mod.rs index 32947d1..be2666f 100644 --- a/imphnen-libs/src/jsonwebtoken/mod.rs +++ b/imphnen-libs/src/jsonwebtoken/mod.rs @@ -11,16 +11,18 @@ pub struct Claims { pub exp: usize, pub iat: usize, pub sub: String, + pub user_id: String, + pub permissions: Vec, } -pub fn encode_access_token(sub: String) -> Result { +pub fn encode_access_token(sub: String, user_id: String, permissions: Vec) -> Result { let env = &ENV; let secret: String = env.access_token_secret.clone(); let now = Utc::now(); let expire: TimeDelta = Duration::minutes(15); let exp: usize = (now + expire).timestamp() as usize; let iat: usize = now.timestamp() as usize; - let claim = Claims { iat, exp, sub }; + let claim = Claims { iat, exp, sub, user_id, permissions }; encode( &Header::default(), &claim, @@ -29,14 +31,14 @@ pub fn encode_access_token(sub: String) -> Result { .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) } -pub fn encode_reset_password_token(sub: String) -> Result { +pub fn encode_reset_password_token(sub: String, user_id: String, permissions: Vec) -> Result { let env = &ENV; let secret: String = env.access_token_secret.clone(); let now = Utc::now(); let expire: TimeDelta = Duration::minutes(5); let exp: usize = (now + expire).timestamp() as usize; let iat: usize = now.timestamp() as usize; - let claim = Claims { iat, exp, sub }; + let claim = Claims { iat, exp, sub, user_id, permissions }; encode( &Header::default(), &claim, @@ -59,14 +61,14 @@ pub fn decode_access_token( result } -pub fn encode_refresh_token(sub: String) -> Result { +pub fn encode_refresh_token(sub: String, user_id: String, permissions: Vec) -> Result { let env = &ENV; let secret: String = env.refresh_token_secret.clone(); let now = Utc::now(); let expire: TimeDelta = Duration::days(1); let exp: usize = (now + expire).timestamp() as usize; let iat: usize = now.timestamp() as usize; - let claim = Claims { iat, exp, sub }; + let claim = Claims { iat, exp, sub, user_id, permissions }; encode( &Header::default(), &claim, @@ -89,6 +91,6 @@ pub fn decode_refresh_token( result // Explicitly return result } -pub fn generate_jwt(user_id: &str) -> Result { - encode_access_token(user_id.to_string()) +pub fn generate_jwt(user_id: &str, permissions: Vec) -> Result { + encode_access_token(user_id.to_string(), user_id.to_string(), permissions) } diff --git a/tests/src/iam/auth/google/google_oauth_flow_test.rs b/tests/src/iam/auth/google/google_oauth_flow_test.rs index e6b1af8..550f74a 100644 --- a/tests/src/iam/auth/google/google_oauth_flow_test.rs +++ b/tests/src/iam/auth/google/google_oauth_flow_test.rs @@ -108,8 +108,8 @@ mod tests { let mut mock_users_service = UsersServiceMock::new(); // Changed from MockUsersServiceMock to UsersServiceMock let user_email = "new.user@example.com".to_string(); - let expected_access_token = generate_jwt("test_user_id").unwrap(); - let expected_refresh_token = generate_jwt("test_user_id").unwrap(); + let expected_access_token = generate_jwt(&user_email, "test_user_id", vec![]).unwrap(); + let expected_refresh_token = generate_jwt(&user_email, "test_user_id", vec![]).unwrap(); let expected_response_dto = AuthLoginResponsetDto { token: TokenDto { @@ -195,8 +195,8 @@ mod tests { let mut mock_users_service = UsersServiceMock::new(); // Changed from MockUsersServiceMock to UsersServiceMock let user_email = "existing.user@example.com".to_string(); - let expected_access_token = generate_jwt("existing_user_id").unwrap(); - let expected_refresh_token = generate_jwt("existing_user_id").unwrap(); + let expected_access_token = generate_jwt(&user_email, "existing_user_id", vec![]).unwrap(); + let expected_refresh_token = generate_jwt(&user_email, "existing_user_id", vec![]).unwrap(); let existing_user_dto = UsersDetailItemDto { // Changed from UserDto to UsersDetailItemDto id: "existing_user_id".to_string(),