nxtgauge-backend-rust/apps/users/src/handlers/admin.rs

181 lines
5.9 KiB
Rust
Raw Normal View History

use crate::AppState;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
Json, Router,
};
use contracts::auth_middleware::{AuthUser, require_admin};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use sqlx::{FromRow, Row};
pub fn router() -> Router<AppState> {
Router::new()
.route("/users", get(list_users))
.route("/customers", get(list_customers))
.route("/candidates", get(list_candidates))
.route("/users/{id}/status", axum::routing::patch(update_user_status))
}
#[derive(Deserialize)]
pub struct ListQuery {
pub q: Option<String>,
pub status: Option<String>,
pub role: Option<String>,
}
#[derive(Serialize, Deserialize, FromRow)]
pub struct AdminUserRow {
pub id: Uuid,
pub email: String,
pub full_name: Option<String>,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub roles: Vec<String>,
}
async fn list_users(
_auth: AuthUser,
State(state): State<AppState>,
Query(q): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let search = q.q.as_deref().unwrap_or_default().to_lowercase();
let role_filter = q.role.as_deref().unwrap_or_default().to_uppercase();
let sql = if role_filter.is_empty() {
// Generic list: users + their approved roles
r#"
SELECT
u.id, u.email, u.full_name, u.status, u.created_at,
COALESCE(array_agg(r.key) FILTER (WHERE r.key IS NOT NULL), '{}') as roles
FROM users u
LEFT JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
LEFT JOIN roles r ON r.id = ur.role_id
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
GROUP BY u.id
ORDER BY u.created_at DESC
LIMIT 100
"#.to_string()
} else {
// Role-specific list: joins with the specific profile table to get THAT role's status
let table = match role_filter.as_str() {
"PHOTOGRAPHER" => "photographer_profiles",
"MAKEUP_ARTIST" => "makeup_artist_profiles",
"TUTOR" => "tutor_profiles",
"DEVELOPER" => "developer_profiles",
"VIDEO_EDITOR" => "video_editor_profiles",
"GRAPHIC_DESIGNER" => "graphic_designer_profiles",
"SOCIAL_MEDIA_MANAGER" => "social_media_manager_profiles",
"FITNESS_TRAINER" => "fitness_trainer_profiles",
"CATERING_SERVICES" => "catering_service_profiles",
"CUSTOMER" => "customer_profiles",
"COMPANY" => "company_profiles",
"JOB_SEEKER" => "job_seeker_profiles",
_ => "user_roles", // fallback
};
format!(
r#"
SELECT
u.id, u.email, u.full_name, p.status, u.created_at,
ARRAY['{}']::text[] as roles
FROM users u
JOIN {} p ON p.user_id = u.id
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
ORDER BY u.created_at DESC
LIMIT 100
"#,
role_filter, table
)
};
let rows = sqlx::query_as::<_, AdminUserRow>(&sql)
.bind(search)
.fetch_all(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(Json(rows))
}
async fn list_customers(
_auth: AuthUser,
State(state): State<AppState>,
Query(q): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let search = q.q.unwrap_or_default().to_lowercase();
let sql = r#"
SELECT
u.id, u.email, u.full_name, u.status, u.created_at,
ARRAY['CUSTOMER']::text[] as roles
FROM users u
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
JOIN roles r ON r.id = ur.role_id AND r.key = 'CUSTOMER'
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
ORDER BY u.created_at DESC
LIMIT 50
"#;
let rows = sqlx::query_as::<_, AdminUserRow>(sql)
.bind(search)
.fetch_all(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(Json(rows))
}
async fn list_candidates(
_auth: AuthUser,
State(state): State<AppState>,
Query(q): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let search = q.q.unwrap_or_default().to_lowercase();
let sql = r#"
SELECT
u.id, u.email, u.full_name, u.status, u.created_at,
ARRAY['JOB_SEEKER']::text[] as roles
FROM users u
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
JOIN roles r ON r.id = ur.role_id AND r.key = 'JOB_SEEKER'
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
ORDER BY u.created_at DESC
LIMIT 50
"#;
let rows = sqlx::query_as::<_, AdminUserRow>(sql)
.bind(search)
.fetch_all(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(Json(rows))
}
#[derive(Deserialize)]
pub struct StatusPayload {
pub status: String,
}
async fn update_user_status(
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<StatusPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
sqlx::query!(
"UPDATE users SET status = $1, updated_at = NOW() WHERE id = $2",
payload.status,
id
)
.execute(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(StatusCode::OK)
}