2026-04-02 13:09:43 +02:00
|
|
|
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))
|
2026-04-03 02:32:46 +02:00
|
|
|
.route("/users/{id}/status", axum::routing::patch(update_user_status))
|
2026-04-02 13:09:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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)
|
|
|
|
|
}
|