chore: checkpoint current workspace changes

This commit is contained in:
Ashwin Kumar 2026-03-22 15:55:29 +01:00
parent cb36e2fa7d
commit 91534d74c0
34 changed files with 240 additions and 97 deletions

View file

@ -41,6 +41,6 @@ prost = "0.13"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }
lettre = { version = "0.11", features = ["tokio1-rustls-tls", "serde"] }
redis = { version = "0.27", features = ["tokio-comp"] }
lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "serde"] }
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
async-trait = "0.1"

View file

@ -8,7 +8,7 @@ axum = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tower-http = { version = "0.6", features = ["proxy", "cors"] }
tower-http = { version = "0.6", features = ["cors"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
reqwest = { version = "0.12", features = ["json", "stream"] }

View file

@ -193,7 +193,7 @@ async fn main() {
let cors = build_cors();
let app = Router::new()
.route("/api/*path", any(proxy_handler))
.route("/api/{*path}", any(proxy_handler))
.route("/health", any(|| async { "Gateway OK" }))
.layer(cors)
.with_state(services);

View file

@ -19,5 +19,5 @@ lettre = { workspace = true }
contracts = { path = "../../crates/contracts" }
cache = { path = "../../crates/cache" }
rand = "0.8"
anyhow = { workspace = true }

View file

@ -15,16 +15,16 @@ use uuid::Uuid;
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(list_pending))
.route("/profiles/company/:user_id/approve", post(approve_company_profile))
.route("/profiles/company/:user_id/reject", post(reject_company_profile))
.route("/profiles/customer/:user_id/approve", post(approve_customer_profile))
.route("/profiles/customer/:user_id/reject", post(reject_customer_profile))
.route("/profiles/professional/:role_key/:user_id/approve", post(approve_professional_profile))
.route("/profiles/professional/:role_key/:user_id/reject", post(reject_professional_profile))
.route("/jobs/:id/approve", post(approve_job))
.route("/jobs/:id/reject", post(reject_job))
.route("/requirements/:id/approve", post(approve_requirement))
.route("/requirements/:id/reject", post(reject_requirement))
.route("/profiles/company/{user_id}/approve", post(approve_company_profile))
.route("/profiles/company/{user_id}/reject", post(reject_company_profile))
.route("/profiles/customer/{user_id}/approve", post(approve_customer_profile))
.route("/profiles/customer/{user_id}/reject", post(reject_customer_profile))
.route("/profiles/professional/{role_key}/{user_id}/approve", post(approve_professional_profile))
.route("/profiles/professional/{role_key}/{user_id}/reject", post(reject_professional_profile))
.route("/jobs/{id}/approve", post(approve_job))
.route("/jobs/{id}/reject", post(reject_job))
.route("/requirements/{id}/approve", post(approve_requirement))
.route("/requirements/{id}/reject", post(reject_requirement))
}
#[derive(Deserialize)]
@ -269,18 +269,18 @@ async fn list_pending(
Json(serde_json::json!({
"jobs": jobs,
"requirements": requirements,
"profiles": {
"company": company_profiles,
"customer": customer_profiles,
"photographer": photographer_profiles,
"makeup_artist": makeup_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_profiles
"profiles_summary": {
"company": company_profiles.len(),
"customer": customer_profiles.len(),
"photographer": photographer_profiles.len(),
"makeup_artist": makeup_profiles.len(),
"tutor": tutor_profiles.len(),
"developer": developer_profiles.len(),
"video_editor": video_editor_profiles.len(),
"graphic_designer": graphic_designer_profiles.len(),
"social_media_manager": social_media_manager_profiles.len(),
"fitness_trainer": fitness_trainer_profiles.len(),
"catering_services": catering_profiles.len()
},
"pagination": { "page": page, "limit": limit }
})),

View file

@ -17,21 +17,21 @@ use uuid::Uuid;
pub fn onboarding_router() -> Router<AppState> {
Router::new()
.route("/", get(list_onboarding_configs).post(create_onboarding_config))
.route("/:role_id", get(get_active_onboarding_config))
.route("/by-key/:role_key", get(get_onboarding_config_by_key))
.route("/{role_id}", get(get_active_onboarding_config))
.route("/by-key/{role_key}", get(get_onboarding_config_by_key))
}
pub fn dashboard_router() -> Router<AppState> {
Router::new()
.route("/", get(list_dashboard_configs).post(create_dashboard_config))
.route("/:role_id", get(get_active_dashboard_config))
.route("/by-key/:role_key", get(get_dashboard_config_by_key))
.route("/{role_id}", get(get_active_dashboard_config))
.route("/by-key/{role_key}", get(get_dashboard_config_by_key))
}
pub fn runtime_router() -> Router<AppState> {
Router::new()
.route("/", get(get_my_runtime_config).post(create_runtime_config))
.route("/:role_id", get(get_active_runtime_config))
.route("/{role_id}", get(get_active_runtime_config))
}
async fn get_my_runtime_config(

View file

@ -13,7 +13,7 @@ pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(list_notifications))
.route("/unread-count", get(unread_count))
.route("/:id/read", patch(mark_read))
.route("/{id}/read", patch(mark_read))
.route("/read-all", patch(mark_all_read))
}

View file

@ -83,8 +83,8 @@ async fn get_state(
let response = match onboarding_state {
Some(s) => serde_json::json!({
"status": s.status,
"currentStep": s.progress_json.as_ref()
.and_then(|p| p.get("step"))
"currentStep": s.progress_json
.get("step")
.and_then(|v| v.as_i64())
.unwrap_or(0),
"progress": s.progress_json,

View file

@ -11,7 +11,7 @@ use db::models::role::{CreateRolePayload, RoleRepository};
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(list_roles).post(create_role))
.route("/:key", get(get_role_by_key))
.route("/{key}", get(get_role_by_key))
}
async fn create_role(

View file

@ -13,4 +13,5 @@ chrono = { workspace = true }
uuid = { workspace = true }
anyhow = { workspace = true }
axum = { workspace = true }
async-trait = { workspace = true }
db = { path = "../db" }

View file

@ -2,34 +2,39 @@ use axum::{
extract::FromRequestParts,
http::{request::Parts, StatusCode},
};
use axum::async_trait;
use std::future::Future;
pub struct RequireAuth(pub crate::jwt::Claims);
#[async_trait]
impl<S> FromRequestParts<S> for RequireAuth
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let auth_header = parts
fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let token = parts
.headers
.get("Authorization")
.and_then(|value| value.to_str().ok())
.filter(|value| value.starts_with("Bearer "));
.filter(|value| value.starts_with("Bearer "))
.map(|header| header.trim_start_matches("Bearer ").to_string());
let token = match auth_header {
Some(header) => header.trim_start_matches("Bearer "),
None => return Err((StatusCode::UNAUTHORIZED, "Missing Bearer token")),
};
async move {
let token = match token {
Some(token) => token,
None => return Err((StatusCode::UNAUTHORIZED, "Missing Bearer token")),
};
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
match crate::jwt::verify_access_token(token, &jwt_secret) {
Ok(claims) => Ok(RequireAuth(claims)),
Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid or expired token")),
match crate::jwt::verify_access_token(&token, &jwt_secret) {
Ok(claims) => Ok(RequireAuth(claims)),
Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid or expired token")),
}
}
}
}

View file

@ -36,7 +36,7 @@ pub async fn invalidate_marketplace(
let pattern = format!("jobs:marketplace:{profession}:*");
let keys: Vec<String> = redis.keys(pattern).await?;
if !keys.is_empty() {
redis.del(keys).await?;
redis.del::<_, ()>(keys).await?;
}
Ok(())
}

View file

@ -43,7 +43,7 @@ pub async fn record_resend(redis: &mut RedisPool, user_id: &str) -> Result<(), r
let count: i64 = redis.incr(&key, 1i64).await?;
// Only set expiry on first increment so window is fixed from first request
if count == 1 {
redis.expire(&key, RESEND_WINDOW_SECS).await?;
redis.expire::<_, ()>(&key, RESEND_WINDOW_SECS).await?;
}
Ok(())
}

View file

@ -24,7 +24,7 @@ pub async fn check(
let key = format!("rate:{namespace}:{identifier}");
let count: i64 = redis.incr(&key, 1i64).await?;
if count == 1 {
redis.expire(&key, window_secs).await?;
redis.expire::<_, ()>(&key, window_secs).await?;
}
Ok(count <= max)
}

View file

@ -11,6 +11,8 @@ tracing = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
anyhow = { workspace = true }
sqlx = { workspace = true }
async-trait = { workspace = true }
jsonwebtoken = "9.3"
db = { path = "../db" }
cache = { path = "../cache" }

View file

@ -6,6 +6,7 @@ use axum::{
};
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use serde::{Deserialize, Serialize};
use std::future::Future;
use uuid::Uuid;
// ── JWT Claims ────────────────────────────────────────────────────────────────
@ -31,49 +32,54 @@ pub struct AuthUser {
pub claims: Claims,
}
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// 1. Extract Authorization header
fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let auth_header = parts
.headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.ok_or(AuthError::MissingToken)?;
.map(str::to_string);
// 2. Strip "Bearer " prefix
let token = auth_header
async move {
let auth_header = auth_header.ok_or(AuthError::MissingToken)?;
// 2. Strip "Bearer " prefix
let token = auth_header
.strip_prefix("Bearer ")
.ok_or(AuthError::InvalidToken)?;
// 3. Decode & verify
let jwt_secret = std::env::var("JWT_SECRET")
// 3. Decode & verify
let jwt_secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET must be set — refusing to start with insecure default");
let token_data = decode::<Claims>(
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256),
)
)
.map_err(|e| {
tracing::debug!("JWT decode error: {}", e);
AuthError::InvalidToken
})?;
// 4. Parse user_id as UUID
let user_id = Uuid::parse_str(&token_data.claims.sub)
// 4. Parse user_id as UUID
let user_id = Uuid::parse_str(&token_data.claims.sub)
.map_err(|_| AuthError::InvalidToken)?;
Ok(AuthUser {
user_id,
email: token_data.claims.email.clone(),
claims: token_data.claims,
})
Ok(AuthUser {
user_id,
email: token_data.claims.email.clone(),
claims: token_data.claims,
})
}
}
}

View file

@ -142,7 +142,10 @@ async fn send_lead_request(
)
.into_response()
}
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => {
tracing::error!("Failed to check profile approval: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to validate profile approval").into_response();
}
}
// ── Deduplication: one lead per requirement per professional (24 h) ────────

View file

@ -204,6 +204,126 @@ ALTER TABLE services
ALTER TABLE lead_requests
ADD COLUMN IF NOT EXISTS professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE;
-- Backfill columns when legacy minimal profile tables already exist.
-- This keeps migrations idempotent while upgrading old schemas to the new profile shape.
ALTER TABLE photographer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS specialties TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS camera_brands TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS outdoor_shoots BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS travel_radius_km INTEGER DEFAULT 50,
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE tutor_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS subjects TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS board_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS qualification VARCHAR(255),
ADD COLUMN IF NOT EXISTS teaches_online BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS teaches_offline BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE makeup_artist_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS specializations TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS kit_brands TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS home_service BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE developer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS tech_stack TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS github_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS availability VARCHAR(50) DEFAULT 'FULL_TIME',
ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS remote_ok BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE video_editor_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS software_skills TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS turnaround_days INTEGER DEFAULT 7,
ADD COLUMN IF NOT EXISTS reel_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE graphic_designer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS design_tools TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS brand_experience BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE social_media_manager_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS platforms TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS industries TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS content_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS avg_follower_growth_pct INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE fitness_trainer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS disciplines TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS certifications TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS online_sessions BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS home_visits BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS gym_based BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS per_session_rate_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE catering_service_profiles
ADD COLUMN IF NOT EXISTS business_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS cuisine_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS event_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS min_guests INTEGER DEFAULT 10,
ADD COLUMN IF NOT EXISTS max_guests INTEGER DEFAULT 500,
ADD COLUMN IF NOT EXISTS has_setup_team BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS has_serving_staff BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS price_per_head_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE lead_requests
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
-- Indexes
CREATE INDEX IF NOT EXISTS idx_photographer_profiles_status ON photographer_profiles(status);
CREATE INDEX IF NOT EXISTS idx_tutor_profiles_status ON tutor_profiles(status);

View file

@ -2,7 +2,7 @@
CREATE TABLE IF NOT EXISTS reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lead_request_id UUID NOT NULL REFERENCES lead_requests(id) ON DELETE CASCADE UNIQUE,
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
customer_id UUID NOT NULL REFERENCES customer_profiles(id) ON DELETE CASCADE,
professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE,
rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,

View file

@ -33,7 +33,7 @@ impl CateringServiceRepository {
sqlx::query_as!(
CateringServiceProfile,
r#"SELECT id, user_id, business_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM catering_service_profiles WHERE user_id = $1"#,
user_id
@ -52,7 +52,7 @@ impl CateringServiceRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, business_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.business_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -183,7 +183,7 @@ impl ConfigRepository {
r#"
UPDATE dashboard_configs
SET is_active = false
WHERE role_id = $1 AND audience = $2 AND is_active = true
WHERE role_id = $1 AND audience = $2::text AND is_active = true
"#,
payload.role_id,
payload.audience
@ -198,9 +198,9 @@ impl ConfigRepository {
INSERT INTO dashboard_configs (role_id, audience, config_json, version, is_active)
VALUES (
$1,
$2,
$2::text,
$3,
COALESCE((SELECT MAX(version) FROM dashboard_configs WHERE role_id = $1 AND audience = $2), 0) + 1,
COALESCE((SELECT MAX(version) FROM dashboard_configs WHERE role_id = $1 AND audience = $2::text), 0) + 1,
true
)
RETURNING id, role_id, audience, config_json, version, is_active, updated_at

View file

@ -11,7 +11,7 @@ pub struct CustomerProfile {
pub phone: Option<String>,
pub city: Option<String>,
pub area: Option<String>,
pub preferred_professions: Vec<String>,
pub preferred_professions: Option<Vec<String>>,
pub active_requirement_count: i32,
pub status: String,
pub bio: Option<String>,

View file

@ -31,7 +31,7 @@ impl DeveloperRepository {
sqlx::query_as!(
DeveloperProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM developer_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl DeveloperRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl FitnessTrainerRepository {
sqlx::query_as!(
FitnessTrainerProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM fitness_trainer_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl FitnessTrainerRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl GraphicDesignerRepository {
sqlx::query_as!(
GraphicDesignerProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM graphic_designer_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl GraphicDesignerRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -15,7 +15,7 @@ pub struct Job {
pub salary_min: Option<i32>,
pub salary_max: Option<i32>,
pub experience_years: Option<i32>,
pub skills: Vec<String>,
pub skills: Option<Vec<String>>,
pub status: String, // DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED
pub rejection_reason: Option<String>,
pub expires_at: Option<DateTime<Utc>>,

View file

@ -10,8 +10,8 @@ pub struct JobSeekerProfile {
pub full_name: Option<String>,
pub location: Option<String>,
pub summary: Option<String>,
pub experience_years: i32,
pub skills: Vec<String>,
pub experience_years: Option<i32>,
pub skills: Option<Vec<String>>,
pub resume_url: Option<String>,
pub active_application_count: i32,
pub status: String,

View file

@ -13,6 +13,8 @@ pub struct LeadRequest {
pub expires_at: DateTime<Utc>,
pub requested_at: DateTime<Utc>,
pub resolved_at: Option<DateTime<Utc>>,
pub professional_user_id: Option<Uuid>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]

View file

@ -31,7 +31,7 @@ impl MakeupArtistRepository {
sqlx::query_as!(
MakeupArtistProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM makeup_artist_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl MakeupArtistRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl PhotographerRepository {
sqlx::query_as!(
PhotographerProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM photographer_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl PhotographerRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -28,6 +28,8 @@ pub struct PortfolioItem {
pub tags: Option<Vec<String>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub user_id: Option<Uuid>,
pub profession_key: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
@ -41,6 +43,8 @@ pub struct Service {
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub user_id: Option<Uuid>,
pub profession_key: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]

View file

@ -31,7 +31,7 @@ impl SocialMediaManagerRepository {
sqlx::query_as!(
SocialMediaManagerProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM social_media_manager_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl SocialMediaManagerRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl TutorRepository {
sqlx::query_as!(
TutorProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM tutor_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl TutorRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl VideoEditorRepository {
sqlx::query_as!(
VideoEditorProfile,
r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at
FROM video_editor_profiles WHERE user_id = $1"#,
user_id
@ -50,7 +50,7 @@ impl VideoEditorRepository {
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>",
custom_data,
status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await