chore: checkpoint current workspace changes
This commit is contained in:
parent
cb36e2fa7d
commit
91534d74c0
34 changed files with 240 additions and 97 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -19,5 +19,5 @@ lettre = { workspace = true }
|
|||
contracts = { path = "../../crates/contracts" }
|
||||
cache = { path = "../../crates/cache" }
|
||||
rand = "0.8"
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ chrono = { workspace = true }
|
|||
uuid = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
db = { path = "../db" }
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
crates/cache/src/jobs.rs
vendored
2
crates/cache/src/jobs.rs
vendored
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
2
crates/cache/src/otp.rs
vendored
2
crates/cache/src/otp.rs
vendored
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
2
crates/cache/src/rate_limit.rs
vendored
2
crates/cache/src/rate_limit.rs
vendored
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) ────────
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue