fix(session1): customer list_requests path arg, external-role by-key endpoint, RuntimeRoleDetail type

This commit is contained in:
Tracewebstudio Dev 2026-06-10 16:19:46 +02:00
parent 2c6d102205
commit 319b384f0a
3 changed files with 108 additions and 16 deletions

View file

@ -222,8 +222,7 @@ async fn submit_requirement(
async fn list_requests( async fn list_requests(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, auth: AuthUser,
_auth: AuthUser,
Query(q): Query<PaginationQuery>, Query(q): Query<PaginationQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let page = q.page.unwrap_or(1); let page = q.page.unwrap_or(1);
@ -233,12 +232,12 @@ async fn list_requests(
let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>( let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>(
r#" r#"
SELECT * FROM lead_requests SELECT * FROM lead_requests
WHERE user_role_profile_id = $1 WHERE professional_user_id = $1
ORDER BY requested_at DESC ORDER BY requested_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"# "#
) )
.bind(id) .bind(auth.user_id)
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(&state.pool) .fetch_all(&state.pool)
@ -271,7 +270,7 @@ async fn approve_request(
Ok(updated) => { Ok(updated) => {
match TracecoinWalletRepository::try_debit_reserved_tracecoins( match TracecoinWalletRepository::try_debit_reserved_tracecoins(
&state.pool, &state.pool,
lead.user_role_profile_id, lead.user_role_profile_id.unwrap(),
lead.tracecoins_reserved, lead.tracecoins_reserved,
lead.id, lead.id,
).await { ).await {
@ -307,7 +306,7 @@ async fn reject_request(
Ok(updated) => { Ok(updated) => {
match TracecoinWalletRepository::try_release_reserved_tracecoins( match TracecoinWalletRepository::try_release_reserved_tracecoins(
&state.pool, &state.pool,
lead.user_role_profile_id, lead.user_role_profile_id.unwrap(),
lead.tracecoins_reserved, lead.tracecoins_reserved,
lead.id, lead.id,
"LEAD_REJECTED", "LEAD_REJECTED",

View file

@ -3,7 +3,7 @@ use axum::{
extract::{Path, Query, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
routing::get, routing::{get, patch},
Json, Router, Json, Router,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -14,6 +14,8 @@ use contracts::auth_middleware::{AuthUser, require_admin};
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_external_roles).post(create_external_role)) .route("/", get(list_external_roles).post(create_external_role))
.route("/by-key/{role_key}", get(get_external_role_by_key))
.route("/by-key/{role_key}", patch(update_external_role_by_key))
.route("/{id}", get(get_external_role).put(update_external_role).delete(delete_external_role)) .route("/{id}", get(get_external_role).put(update_external_role).delete(delete_external_role))
} }
@ -247,6 +249,60 @@ async fn get_external_role(
})) }))
} }
async fn get_external_role_by_key(
auth: AuthUser,
State(state): State<AppState>,
Path(role_key): Path<String>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
r#"
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
rc.updated_at as updated_at, rc.config_json as config_json
FROM roles r
LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
WHERE r.key = $1 AND r.audience = 'EXTERNAL'
"#,
)
.bind(&role_key)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
Ok(Json(ExternalRoleDetail {
id: row.id,
name: row.name,
code: row.code,
audience: row.audience,
is_active: row.is_active,
runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
created_at: row.created_at,
updated_at: row.updated_at,
}))
}
async fn update_external_role_by_key(
auth: AuthUser,
State(state): State<AppState>,
Path(role_key): Path<String>,
Json(payload): Json<UpdateExternalRolePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let row: (Uuid,) = sqlx::query_as("SELECT id FROM roles WHERE key = $1 AND audience = 'EXTERNAL'")
.bind(&role_key)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
update_external_role_impl(&state, row.0, payload).await
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct CreateExternalRolePayload { struct CreateExternalRolePayload {
name: String, name: String,
@ -340,6 +396,14 @@ async fn update_external_role(
if let Err(_e) = require_admin(&auth) { if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
} }
update_external_role_impl(&state, id, payload).await
}
async fn update_external_role_impl(
state: &AppState,
role_id: Uuid,
payload: UpdateExternalRolePayload,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if payload.name.is_some() || payload.is_active.is_some() { if payload.name.is_some() || payload.is_active.is_some() {
sqlx::query( sqlx::query(
r#" r#"
@ -351,7 +415,7 @@ async fn update_external_role(
) )
.bind(payload.name) .bind(payload.name)
.bind(payload.is_active) .bind(payload.is_active)
.bind(id) .bind(role_id)
.execute(&state.pool) .execute(&state.pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -364,7 +428,7 @@ async fn update_external_role(
WHERE role_id = $1 AND is_active = true WHERE role_id = $1 AND is_active = true
"#, "#,
) )
.bind(id) .bind(role_id)
.execute(&state.pool) .execute(&state.pool)
.await .await
.ok(); .ok();
@ -379,13 +443,38 @@ async fn update_external_role(
) )
"#, "#,
) )
.bind(id) .bind(role_id)
.bind(runtime) .bind(runtime)
.execute(&state.pool) .execute(&state.pool)
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
} }
get_external_role(auth, State(state), Path(id)).await // Return the updated role detail
let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
r#"
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
rc.updated_at as updated_at, rc.config_json as config_json
FROM roles r
LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
WHERE r.id = $1 AND r.audience = 'EXTERNAL'
"#,
)
.bind(role_id)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
Ok(Json(ExternalRoleDetail {
id: row.id,
name: row.name,
code: row.code,
audience: row.audience,
is_active: row.is_active,
runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
created_at: row.created_at,
updated_at: row.updated_at,
}))
} }
async fn delete_external_role( async fn delete_external_role(

View file

@ -32,6 +32,8 @@ pub struct PaginationQuery {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct LeadRequestPayload { pub struct LeadRequestPayload {
pub requirement_id: Uuid, pub requirement_id: Uuid,
#[serde(default)]
pub message: Option<String>,
} }
/// Build the shared Router that every profession service merges into its own Router. /// Build the shared Router that every profession service merges into its own Router.
@ -189,10 +191,12 @@ async fn send_lead_request(
return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response(); return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response();
} }
let db_payload = CreateLeadRequestPayload { let db_payload = CreateLeadRequestPayload::new(
user_role_profile_id: user_role_profile.id, req.id,
expires_at: Utc::now() + chrono::Duration::hours(24), user_role_profile.id,
}; auth.user_id,
payload.message.clone(),
);
match LeadRequestRepository::create(&state.pool, db_payload).await { match LeadRequestRepository::create(&state.pool, db_payload).await {
Ok(lead) => { Ok(lead) => {
@ -456,7 +460,7 @@ async fn cancel_request(
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
}; };
if lead.user_role_profile_id != user_role_profile.id { if lead.user_role_profile_id != Some(user_role_profile.id) {
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Access denied" }))).into_response(); return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Access denied" }))).into_response();
} }