648 lines
21 KiB
Rust
648 lines
21 KiB
Rust
use crate::AppState;
|
|
use axum::{
|
|
extract::{Query, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use contracts::auth_middleware::AuthUser;
|
|
use db::models::{role::RoleRepository, user::UserRepository, verification::VerificationRepository};
|
|
use serde::Deserialize;
|
|
use uuid::Uuid;
|
|
|
|
// ── Routers ───────────────────────────────────────────────────────────────────
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/", get(get_profile).patch(save_profile))
|
|
.route("/submit-for-verification", post(submit_for_verification))
|
|
}
|
|
|
|
pub fn me_router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/", get(get_me))
|
|
.route("/verification-status", get(verification_status))
|
|
}
|
|
|
|
// ── DTOs ──────────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct RoleKeyQuery {
|
|
#[serde(rename = "roleKey", alias = "role_key")]
|
|
pub role_key: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct SaveProfileInput {
|
|
#[serde(rename = "roleKey", alias = "role_key")]
|
|
pub role_key: String,
|
|
pub profile_data: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct SubmitInput {
|
|
#[serde(rename = "roleKey", alias = "role_key")]
|
|
pub role_key: String,
|
|
/// Optional: if provided, saves this data before submitting.
|
|
/// If omitted, reads previously saved profile data from DB.
|
|
pub profile_data: Option<serde_json::Value>,
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
fn role_to_table(role_key: &str) -> Option<&'static str> {
|
|
match role_key.to_uppercase().as_str() {
|
|
"PHOTOGRAPHER" => Some("photographer_profiles"),
|
|
"MAKEUP_ARTIST" => Some("makeup_artist_profiles"),
|
|
"TUTOR" => Some("tutor_profiles"),
|
|
"DEVELOPER" => Some("developer_profiles"),
|
|
"VIDEO_EDITOR" => Some("video_editor_profiles"),
|
|
"GRAPHIC_DESIGNER" => Some("graphic_designer_profiles"),
|
|
"SOCIAL_MEDIA_MANAGER" => Some("social_media_manager_profiles"),
|
|
"FITNESS_TRAINER" => Some("fitness_trainer_profiles"),
|
|
"CATERING_SERVICES" => Some("catering_service_profiles"),
|
|
"UGC_CONTENT_CREATOR" => Some("ugc_content_creator_profiles"),
|
|
"JOB_SEEKER" | "JOBSEEKER" => Some("job_seeker_profiles"),
|
|
"CUSTOMER" => Some("customer_profiles"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn is_dummy_account_email(email: &str) -> bool {
|
|
email.ends_with("@demo.com")
|
|
|| email == "paymentgateway@demo.com"
|
|
|| email.contains("+dummy@")
|
|
|| email.starts_with("dummy+")
|
|
}
|
|
|
|
fn extract_documents(profile_data: &serde_json::Value) -> serde_json::Value {
|
|
let doc_keys = [
|
|
"aadhar_doc",
|
|
"registration_doc",
|
|
"gst_doc",
|
|
"sample_work",
|
|
"degree_certificate",
|
|
"certification_doc",
|
|
"fssai_license",
|
|
"identity_proof",
|
|
"address_proof",
|
|
"portfolio_proof",
|
|
];
|
|
let mut docs = vec![];
|
|
for key in &doc_keys {
|
|
if let Some(val) = profile_data.get(key) {
|
|
if !val.is_null() {
|
|
docs.push(serde_json::json!({
|
|
"type": key,
|
|
"value": val,
|
|
"status": "SUBMITTED"
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
serde_json::Value::Array(docs)
|
|
}
|
|
|
|
fn resolve_role_key(auth_role: &str, query_role: Option<String>) -> String {
|
|
query_role
|
|
.filter(|k| !k.is_empty())
|
|
.unwrap_or_else(|| auth_role.to_string())
|
|
.to_uppercase()
|
|
}
|
|
|
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
|
|
/// GET /api/profile?roleKey=PHOTOGRAPHER
|
|
async fn get_profile(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Query(q): Query<RoleKeyQuery>,
|
|
) -> impl IntoResponse {
|
|
let role_key = resolve_role_key(&auth.claims.active_role, q.role_key);
|
|
|
|
if role_key == "COMPANY" {
|
|
let row = sqlx::query(
|
|
r#"SELECT company_name, status, updated_at FROM company_profiles WHERE user_id = $1"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.fetch_optional(&state.pool)
|
|
.await;
|
|
|
|
return match row {
|
|
Ok(Some(r)) => {
|
|
use sqlx::Row;
|
|
let name: Option<String> = r.try_get("company_name").ok();
|
|
let status: String = r.try_get("status").unwrap_or_default();
|
|
(
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"role_key": role_key,
|
|
"profile_data": { "company_name": name },
|
|
"verification_status": status,
|
|
})),
|
|
)
|
|
.into_response()
|
|
}
|
|
Ok(None) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"role_key": role_key,
|
|
"profile_data": null,
|
|
"verification_status": "NOT_STARTED",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
};
|
|
}
|
|
|
|
let table = match role_to_table(&role_key) {
|
|
Some(t) => t,
|
|
None => {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({ "error": format!("Unknown role: {}", role_key) })),
|
|
)
|
|
.into_response()
|
|
}
|
|
};
|
|
|
|
let query = format!(
|
|
r#"SELECT custom_data, status FROM {} WHERE id = $1"#,
|
|
table
|
|
);
|
|
|
|
let user_role_profile_id = match get_user_role_profile_id(&state.pool, auth.user_id, &role_key).await {
|
|
Ok(Some(id)) => id,
|
|
Ok(None) => {
|
|
return (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"role_key": role_key,
|
|
"profile_data": null,
|
|
"verification_status": "NOT_STARTED",
|
|
})),
|
|
)
|
|
.into_response();
|
|
}
|
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
};
|
|
|
|
match sqlx::query(&query)
|
|
.bind(user_role_profile_id)
|
|
.fetch_optional(&state.pool)
|
|
.await
|
|
{
|
|
Ok(Some(row)) => {
|
|
use sqlx::Row;
|
|
let profile_data: serde_json::Value = row
|
|
.try_get("custom_data")
|
|
.unwrap_or(serde_json::Value::Null);
|
|
let verification_status: String =
|
|
row.try_get("status").unwrap_or_default();
|
|
(
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"role_key": role_key,
|
|
"profile_data": profile_data,
|
|
"verification_status": verification_status,
|
|
})),
|
|
)
|
|
.into_response()
|
|
}
|
|
Ok(None) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"role_key": role_key,
|
|
"profile_data": null,
|
|
"verification_status": "NOT_STARTED",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
/// PATCH /api/profile
|
|
async fn save_profile(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(input): Json<SaveProfileInput>,
|
|
) -> impl IntoResponse {
|
|
let role_key = input.role_key.to_uppercase();
|
|
|
|
if role_key == "COMPANY" {
|
|
let name = input
|
|
.profile_data
|
|
.get("company_name")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default()
|
|
.to_string();
|
|
|
|
return match sqlx::query(
|
|
r#"
|
|
INSERT INTO company_profiles (user_id, company_name, status, updated_at)
|
|
VALUES ($1, $2, 'DRAFT', NOW())
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
company_name = EXCLUDED.company_name,
|
|
updated_at = NOW()
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(&name)
|
|
.execute(&state.pool)
|
|
.await
|
|
{
|
|
Ok(_) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({ "saved": true, "role_key": role_key })),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
};
|
|
}
|
|
|
|
let table = match role_to_table(&role_key) {
|
|
Some(t) => t,
|
|
None => {
|
|
return (
|
|
StatusCode::BAD_REQUEST,
|
|
Json(serde_json::json!({ "error": format!("Unknown role: {}", role_key) })),
|
|
)
|
|
.into_response()
|
|
}
|
|
};
|
|
|
|
let query = format!(
|
|
r#"
|
|
INSERT INTO {table} (id, custom_data, status, updated_at)
|
|
VALUES ($1, $2, 'DRAFT', NOW())
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
custom_data = EXCLUDED.custom_data,
|
|
updated_at = NOW()
|
|
"#
|
|
);
|
|
|
|
let user_role_profile_id = match get_or_create_user_role_profile_id(&state.pool, auth.user_id, &role_key).await {
|
|
Ok(id) => id,
|
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
};
|
|
|
|
match sqlx::query(&query)
|
|
.bind(user_role_profile_id)
|
|
.bind(&input.profile_data)
|
|
.execute(&state.pool)
|
|
.await
|
|
{
|
|
Ok(_) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({ "saved": true, "role_key": role_key })),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
/// POST /api/profile/submit-for-verification
|
|
async fn submit_for_verification(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Json(input): Json<SubmitInput>,
|
|
) -> impl IntoResponse {
|
|
let role_key = input.role_key.to_uppercase();
|
|
|
|
// Check if user is a demo account
|
|
let is_demo = sqlx::query_scalar::<_, String>("SELECT email FROM users WHERE id = $1")
|
|
.bind(auth.user_id)
|
|
.fetch_one(&state.pool)
|
|
.await
|
|
.map(|email| is_dummy_account_email(&email))
|
|
.unwrap_or(false);
|
|
|
|
// For demo accounts: auto-approve verification
|
|
if is_demo {
|
|
tracing::info!(user_id = %auth.user_id, role_key = %role_key, "Demo account auto-approved for verification");
|
|
|
|
// Update role assignment to APPROVED
|
|
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
|
sqlx::query(
|
|
"UPDATE user_role_assignments SET status = 'APPROVED' WHERE user_id = $1 AND role_id = $2",
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(role.id)
|
|
.execute(&state.pool)
|
|
.await
|
|
.ok();
|
|
}
|
|
|
|
// Mark profile as VERIFIED
|
|
set_profile_status(&state, auth.user_id, &role_key, "VERIFIED").await;
|
|
|
|
// Create a verification record with APPROVED status
|
|
let profile_data = input.profile_data.unwrap_or_else(|| {
|
|
serde_json::json!({
|
|
"company_name": "Payment Gateway Demo Company",
|
|
"company_description": "Demo account for reviewing packages",
|
|
"industry": "Technology",
|
|
"location": "India"
|
|
})
|
|
});
|
|
let documents = extract_documents(&profile_data);
|
|
|
|
match VerificationRepository::create_approved(
|
|
&state.pool,
|
|
auth.user_id,
|
|
&role_key,
|
|
"PROFILE_VERIFICATION",
|
|
profile_data,
|
|
documents,
|
|
)
|
|
.await
|
|
{
|
|
Ok(v) => (
|
|
StatusCode::CREATED,
|
|
Json(serde_json::json!({
|
|
"verification_id": v.id,
|
|
"status": "APPROVED",
|
|
"message": "Your profile has been auto-approved for demo access."
|
|
})),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
} else {
|
|
// Regular verification flow for non-demo accounts
|
|
// Guard: reject if an active verification already exists
|
|
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
|
r#"
|
|
SELECT id FROM verifications
|
|
WHERE user_id = $1 AND role_key = $2
|
|
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
|
LIMIT 1
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(&role_key)
|
|
.fetch_optional(&state.pool)
|
|
.await;
|
|
|
|
if existing.unwrap_or(None).is_some() {
|
|
return (
|
|
StatusCode::CONFLICT,
|
|
Json(serde_json::json!({
|
|
"error": "A verification is already in progress for this role. Please wait for it to be reviewed."
|
|
})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
// Fetch saved profile data or use submitted data
|
|
let profile_data = match input.profile_data {
|
|
Some(data) => data,
|
|
None => fetch_saved_profile(&state, auth.user_id, &role_key).await,
|
|
};
|
|
|
|
let documents = extract_documents(&profile_data);
|
|
|
|
// Mark profile as PENDING in role-specific table
|
|
set_profile_status(&state, auth.user_id, &role_key, "PENDING").await;
|
|
|
|
// Mark user_role as PENDING
|
|
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
|
sqlx::query(
|
|
"UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(role.id)
|
|
.execute(&state.pool)
|
|
.await
|
|
.ok();
|
|
}
|
|
|
|
// Create verification record — appears in admin Verification Management
|
|
match VerificationRepository::create(
|
|
&state.pool,
|
|
auth.user_id,
|
|
&role_key,
|
|
"PROFILE_VERIFICATION",
|
|
"MEDIUM",
|
|
profile_data,
|
|
documents,
|
|
)
|
|
.await
|
|
{
|
|
Ok(v) => (
|
|
StatusCode::CREATED,
|
|
Json(serde_json::json!({
|
|
"verification_id": v.id,
|
|
"status": v.status,
|
|
"message": "Your profile has been submitted for verification. We will notify you once it has been reviewed."
|
|
})),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// GET /api/me/verification-status?roleKey=PHOTOGRAPHER
|
|
pub async fn verification_status(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
Query(q): Query<RoleKeyQuery>,
|
|
) -> impl IntoResponse {
|
|
let role_key = resolve_role_key(&auth.claims.active_role, q.role_key);
|
|
|
|
let row = sqlx::query(
|
|
r#"
|
|
SELECT id, status, notes, rejection_reason, updated_at
|
|
FROM verifications
|
|
WHERE user_id = $1 AND role_key = $2
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
"#,
|
|
)
|
|
.bind(auth.user_id)
|
|
.bind(&role_key)
|
|
.fetch_optional(&state.pool)
|
|
.await;
|
|
|
|
match row {
|
|
Ok(Some(r)) => {
|
|
use sqlx::Row;
|
|
let id: Uuid = r.try_get("id").unwrap_or(Uuid::nil());
|
|
let status: String = r.try_get("status").unwrap_or_default();
|
|
let notes: Option<String> = r.try_get("notes").ok().flatten();
|
|
let rejection_reason: Option<String> = r.try_get("rejection_reason").ok().flatten();
|
|
let updated_at: Option<chrono::DateTime<chrono::Utc>> =
|
|
r.try_get("updated_at").ok();
|
|
|
|
(
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"has_verification": true,
|
|
"verification_id": id,
|
|
"status": status,
|
|
"document_request": notes,
|
|
"rejection_reason": rejection_reason,
|
|
"updated_at": updated_at,
|
|
})),
|
|
)
|
|
.into_response()
|
|
}
|
|
Ok(None) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"has_verification": false,
|
|
"status": "NOT_SUBMITTED",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
|
|
async fn fetch_saved_profile(
|
|
state: &AppState,
|
|
user_id: Uuid,
|
|
role_key: &str,
|
|
) -> serde_json::Value {
|
|
if role_key == "COMPANY" {
|
|
return match sqlx::query(r#"SELECT company_name FROM company_profiles WHERE user_id = $1"#)
|
|
.bind(user_id)
|
|
.fetch_optional(&state.pool)
|
|
.await
|
|
{
|
|
Ok(Some(r)) => {
|
|
use sqlx::Row;
|
|
let name: Option<String> = r.try_get("company_name").ok();
|
|
serde_json::json!({ "company_name": name })
|
|
}
|
|
_ => serde_json::Value::Object(Default::default()),
|
|
};
|
|
}
|
|
|
|
if let Some(urp_id) = get_user_role_profile_id(&state.pool, user_id, role_key).await.ok().flatten() {
|
|
return fetch_saved_profile_by_urp_id(state, urp_id, role_key).await;
|
|
}
|
|
|
|
serde_json::Value::Object(Default::default())
|
|
}
|
|
|
|
async fn set_profile_status(state: &AppState, user_id: Uuid, role_key: &str, status: &str) {
|
|
if role_key == "COMPANY" {
|
|
sqlx::query(
|
|
r#"UPDATE company_profiles SET status = $1, updated_at = NOW() WHERE user_id = $2"#,
|
|
)
|
|
.bind(status)
|
|
.bind(user_id)
|
|
.execute(&state.pool)
|
|
.await
|
|
.ok();
|
|
return;
|
|
}
|
|
|
|
let user_role_profile_id = match get_user_role_profile_id(&state.pool, user_id, role_key).await {
|
|
Ok(Some(id)) => id,
|
|
Ok(None) => return,
|
|
Err(_) => return,
|
|
};
|
|
|
|
if let Some(table) = role_to_table(role_key) {
|
|
let q = format!(
|
|
"UPDATE {} SET status = $1, updated_at = NOW() WHERE id = $2",
|
|
table
|
|
);
|
|
sqlx::query(&q)
|
|
.bind(status)
|
|
.bind(user_role_profile_id)
|
|
.execute(&state.pool)
|
|
.await
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
async fn get_user_role_profile_id(
|
|
pool: &sqlx::PgPool,
|
|
user_id: Uuid,
|
|
role_key: &str,
|
|
) -> Result<Option<Uuid>, sqlx::Error> {
|
|
sqlx::query_scalar::<_, Uuid>(
|
|
r#"
|
|
SELECT id FROM user_role_profiles
|
|
WHERE user_id = $1 AND role_key = $2
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(role_key)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|
|
|
|
async fn get_or_create_user_role_profile_id(
|
|
pool: &sqlx::PgPool,
|
|
user_id: Uuid,
|
|
role_key: &str,
|
|
) -> Result<Uuid, sqlx::Error> {
|
|
if let Some(id) = get_user_role_profile_id(pool, user_id, role_key).await? {
|
|
return Ok(id);
|
|
}
|
|
|
|
let _role = RoleRepository::get_by_key(pool, role_key).await?;
|
|
|
|
sqlx::query_scalar::<_, Uuid>(
|
|
r#"
|
|
INSERT INTO user_role_profiles (user_id, role_key, status)
|
|
VALUES ($1, $2, 'DRAFT')
|
|
ON CONFLICT (user_id, role_key) DO UPDATE SET updated_at = NOW()
|
|
RETURNING id
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(role_key)
|
|
.fetch_one(pool)
|
|
.await
|
|
}
|
|
|
|
async fn fetch_saved_profile_by_urp_id(
|
|
state: &AppState,
|
|
user_role_profile_id: Uuid,
|
|
role_key: &str,
|
|
) -> serde_json::Value {
|
|
if let Some(table) = role_to_table(role_key) {
|
|
let q = format!(r#"SELECT custom_data FROM {} WHERE id = $1"#, table);
|
|
if let Ok(Some(row)) = sqlx::query(&q)
|
|
.bind(user_role_profile_id)
|
|
.fetch_optional(&state.pool)
|
|
.await
|
|
{
|
|
use sqlx::Row;
|
|
return row
|
|
.try_get::<serde_json::Value, _>("custom_data")
|
|
.unwrap_or(serde_json::Value::Object(Default::default()));
|
|
}
|
|
}
|
|
serde_json::Value::Object(Default::default())
|
|
}
|
|
|
|
/// GET /api/me — returns the authenticated user's basic info
|
|
pub async fn get_me(
|
|
auth: AuthUser,
|
|
State(state): State<AppState>,
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
let user = UserRepository::get_by_id(&state.pool, auth.user_id)
|
|
.await
|
|
.map_err(|_| (StatusCode::UNAUTHORIZED, "User not found".to_string()))?;
|
|
|
|
Ok(Json(serde_json::json!({
|
|
"id": user.id,
|
|
"email": user.email,
|
|
"firstName": user.first_name,
|
|
"lastName": user.last_name,
|
|
"activeRole": auth.claims.active_role,
|
|
"emailVerified": user.email_verified,
|
|
})))
|
|
}
|