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(
State(state): State<AppState>,
Path(id): Path<Uuid>,
_auth: AuthUser,
auth: AuthUser,
Query(q): Query<PaginationQuery>,
) -> impl IntoResponse {
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>(
r#"
SELECT * FROM lead_requests
WHERE user_role_profile_id = $1
WHERE professional_user_id = $1
ORDER BY requested_at DESC
LIMIT $2 OFFSET $3
"#
)
.bind(id)
.bind(auth.user_id)
.bind(limit)
.bind(offset)
.fetch_all(&state.pool)
@ -271,7 +270,7 @@ async fn approve_request(
Ok(updated) => {
match TracecoinWalletRepository::try_debit_reserved_tracecoins(
&state.pool,
lead.user_role_profile_id,
lead.user_role_profile_id.unwrap(),
lead.tracecoins_reserved,
lead.id,
).await {
@ -307,7 +306,7 @@ async fn reject_request(
Ok(updated) => {
match TracecoinWalletRepository::try_release_reserved_tracecoins(
&state.pool,
lead.user_role_profile_id,
lead.user_role_profile_id.unwrap(),
lead.tracecoins_reserved,
lead.id,
"LEAD_REJECTED",

View file

@ -3,7 +3,7 @@ use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
routing::{get, patch},
Json, Router,
};
use serde::{Deserialize, Serialize};
@ -14,6 +14,8 @@ use contracts::auth_middleware::{AuthUser, require_admin};
pub fn router() -> Router<AppState> {
Router::new()
.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))
}
@ -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)]
struct CreateExternalRolePayload {
name: String,
@ -340,6 +396,14 @@ async fn update_external_role(
if let Err(_e) = require_admin(&auth) {
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() {
sqlx::query(
r#"
@ -351,7 +415,7 @@ async fn update_external_role(
)
.bind(payload.name)
.bind(payload.is_active)
.bind(id)
.bind(role_id)
.execute(&state.pool)
.await
.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
"#,
)
.bind(id)
.bind(role_id)
.execute(&state.pool)
.await
.ok();
@ -379,13 +443,38 @@ async fn update_external_role(
)
"#,
)
.bind(id)
.bind(role_id)
.bind(runtime)
.execute(&state.pool)
.await
.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(

View file

@ -32,6 +32,8 @@ pub struct PaginationQuery {
#[derive(Deserialize)]
pub struct LeadRequestPayload {
pub requirement_id: Uuid,
#[serde(default)]
pub message: Option<String>,
}
/// 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();
}
let db_payload = CreateLeadRequestPayload {
user_role_profile_id: user_role_profile.id,
expires_at: Utc::now() + chrono::Duration::hours(24),
};
let db_payload = CreateLeadRequestPayload::new(
req.id,
user_role_profile.id,
auth.user_id,
payload.message.clone(),
);
match LeadRequestRepository::create(&state.pool, db_payload).await {
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(),
};
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();
}