nxtgauge-backend-rust/apps/users/src/handlers/user_roles.rs
Tracewebstudio Dev 2c6d102205 fix(e2e): 14 bug fixes across users, leads, gateway, KB, and reviews
DB:
- Add niche_tags column to ugc_content_creator_profiles (was blocking UGC service)
- Add turnaround_days and fix user_role_profile_id NOT NULL for UGC
- leads/lead_requests tables (already created in session 1)

Code:
- Add UGC_CONTENT_CREATOR to is_professional_role() to auto-create user_role_profiles
- Fix onboarding INSERT to include user_id for photographer_profiles
- Fix send_lead_request_ai to use correct customer_user_id (was self-notifying)
- Add PATCH /api/leads/:id support + mount leads at /api/* for gateway compatibility
- Fix admin_list_cases query (WHERE was using wrong params)
- Fix admin_get_case query (was using list query instead of fetch-by-id)
- Add GET /api/me in profile.rs (moved from onboarding)
- Add KB articles by ID route /api/kb/articles/id/{id}
- Rewrite reviews handlers to match actual reviews table schema
- Add public reviews router GET /api/reviews

Gateway:
- Add /api/reviews route to users service
2026-06-10 16:17:10 +02:00

136 lines
3.8 KiB
Rust

use crate::AppState;
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use contracts::auth_middleware::AuthUser;
use db::models::role::RoleRepository;
use db::models::user_role_profile::UserRoleProfileRepository;
use serde::{Deserialize, Serialize};
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(list_my_roles))
.route("/register", post(register_role))
}
#[derive(Deserialize)]
pub struct RegisterRolePayload {
pub role_key: String,
}
#[derive(Serialize)]
pub struct UserRoleResponse {
pub role_key: String,
pub role_name: String,
pub status: String,
pub approved_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[allow(dead_code)]
fn is_professional_role(role_key: &str) -> bool {
matches!(
role_key,
"PHOTOGRAPHER"
| "MAKEUP_ARTIST"
| "TUTOR"
| "DEVELOPER"
| "VIDEO_EDITOR"
| "GRAPHIC_DESIGNER"
| "SOCIAL_MEDIA_MANAGER"
| "FITNESS_TRAINER"
| "CATERING_SERVICES"
| "UGC_CONTENT_CREATOR"
)
}
#[derive(sqlx::FromRow)]
struct UserRoleRow {
key: String,
name: String,
status: String,
approved_at: Option<chrono::DateTime<chrono::Utc>>,
}
async fn list_my_roles(
auth: AuthUser,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let rows = sqlx::query_as::<_, UserRoleRow>(
r#"
SELECT r.key, r.name, ur.status, ur.approved_at
FROM user_role_assignments ur
INNER JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = $1
ORDER BY ur.created_at ASC
"#,
)
.bind(auth.user_id)
.fetch_all(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let mapped = rows
.into_iter()
.map(|r| UserRoleResponse {
role_key: r.key,
role_name: r.name,
status: r.status,
approved_at: r.approved_at,
})
.collect::<Vec<_>>();
Ok((StatusCode::OK, Json(mapped)))
}
async fn register_role(
auth: AuthUser,
State(state): State<AppState>,
Json(payload): Json<RegisterRolePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let role_key = payload.role_key.trim().to_uppercase();
if role_key.is_empty() {
return Err((StatusCode::BAD_REQUEST, "role_key is required".to_string()));
}
let role = RoleRepository::get_by_key(&state.pool, &role_key)
.await
.map_err(|_| (StatusCode::NOT_FOUND, format!("Role '{}' not found", role_key)))?;
sqlx::query(
r#"
INSERT INTO user_role_assignments (user_id, role_id, status, approved_at)
VALUES ($1, $2, 'APPROVED', NOW())
ON CONFLICT (user_id, role_id)
DO UPDATE SET status = 'APPROVED', approved_at = NOW()
"#,
)
.bind(auth.user_id)
.bind(role.id)
.execute(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// For professional/external roles, also create the user_role_profiles entry so
// downstream services (e.g. leads) can find the professional's profile.
if is_professional_role(&role_key) {
if let Err(e) = UserRoleProfileRepository::create(&state.pool, auth.user_id, &role_key).await {
tracing::warn!("Failed to create user_role_profiles entry for {}: {}", role_key, e);
// Non-fatal — the assignment is created; the profile row can be backfilled later.
}
}
Ok((
StatusCode::OK,
Json(serde_json::json!({
"message": "Role registered successfully",
"role_key": role_key,
"role_id": role.id,
"user_id": auth.user_id.to_string(),
"status": "APPROVED"
})),
))
}