diff --git a/.woodpecker.yml b/.woodpecker.yml index 16e9e3b..53493c4 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -7,6 +7,8 @@ matrix: - gateway - users - companies + - jobs + - leads - job-seekers - customers - payments diff --git a/Cargo.toml b/Cargo.toml index 8c954d6..dcf1d42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ members = [ "apps/cron", "apps/employees", "apps/payments", + "apps/jobs", + "apps/leads", "crates/db-migrate" ] @@ -51,3 +53,4 @@ lettre = { version = "0.11", default-features = false, features = ["tokio1-rustl redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] } async-trait = "0.1" bytes = "1" +tower-http = "0.6" diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index f305c0a..3433561 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -14,6 +14,8 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; struct Services { users_url: String, companies_url: String, + jobs_url: String, + leads_url: String, job_seekers_url: String, customers_url: String, // ── 9 separate profession services ──────────────────────────────────── @@ -41,6 +43,10 @@ impl Services { .unwrap_or_else(|_| "http://localhost:9101".to_string()), companies_url: std::env::var("COMPANIES_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:9102".to_string()), + jobs_url: std::env::var("JOBS_SERVICE_URL") + .unwrap_or_else(|_| "http://localhost:9103".to_string()), + leads_url: std::env::var("LEADS_SERVICE_URL") + .unwrap_or_else(|_| "http://localhost:9118".to_string()), job_seekers_url: std::env::var("JOB_SEEKERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:9104".to_string()), customers_url: std::env::var("CUSTOMERS_SERVICE_URL") @@ -115,17 +121,27 @@ impl Services { { Some(self.employees_url.clone()) } - // Companies + Jobs + Applications + Packages + // Companies + Applications + Packages else if path.starts_with("/api/companies") - || path.starts_with("/api/jobs") || path.starts_with("/api/applications") || path.starts_with("/api/pricing") || path.starts_with("/api/admin/companies") - || path.starts_with("/api/admin/jobs") || path.starts_with("/api/admin/applications") { Some(self.companies_url.clone()) } + // Jobs (separate service) + else if path.starts_with("/api/jobs") + || path.starts_with("/api/admin/jobs") + { + Some(self.jobs_url.clone()) + } + // Leads (separate service) + else if path.starts_with("/api/leads") + || path.starts_with("/api/admin/leads") + { + Some(self.leads_url.clone()) + } // Job Seekers else if path.starts_with("/api/jobseeker") { Some(self.job_seekers_url.clone()) diff --git a/apps/jobs/Cargo.toml b/apps/jobs/Cargo.toml new file mode 100644 index 0000000..72c437c --- /dev/null +++ b/apps/jobs/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "jobs" +version = "0.1.0" +edition = "2021" + +[dependencies] +sqlx = { workspace = true } +axum = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +anyhow = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +tower-http = { version = "0.6", features = ["cors", "trace"] } + +[[bin]] +name = "jobs" +path = "src/main.rs" diff --git a/apps/jobs/src/main.rs b/apps/jobs/src/main.rs new file mode 100644 index 0000000..3cd4ff7 --- /dev/null +++ b/apps/jobs/src/main.rs @@ -0,0 +1,136 @@ +use axum::{ + extract::State, + http::StatusCode, + routing::{get, post, put, delete}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use std::net::SocketAddr; +use std::sync::Arc; +use tower_http::cors::{Any, CorsLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[derive(Clone)] +pub struct AppState { + pub pool: PgPool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Job { + pub id: uuid::Uuid, + pub title: String, + pub description: String, + pub location: String, + pub job_type: String, + pub status: String, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateJob { + pub title: String, + pub description: String, + pub location: String, + pub job_type: String, +} + +async fn list_jobs(State(state): State>) -> Result>, StatusCode> { + let jobs = sqlx::query_as::<_, Job>( + "SELECT id, title, description, location, job_type, status, created_at FROM jobs ORDER BY created_at DESC" + ) + .fetch_all(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(jobs)) +} + +async fn create_job( + State(state): State>, + Json(payload): Json, +) -> Result, StatusCode> { + let job = sqlx::query_as::<_, Job>( + r#" + INSERT INTO jobs (title, description, location, job_type) + VALUES ($1, $2, $3, $4) + RETURNING id, title, description, location, job_type, status, created_at + "#, + ) + .bind(&payload.title) + .bind(&payload.description) + .bind(&payload.location) + .bind(&payload.job_type) + .fetch_one(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(job)) +} + +async fn get_job( + State(state): State>, + axum::extract::Path(id): axum::extract::Path, +) -> Result, StatusCode> { + let job = sqlx::query_as::<_, Job>( + "SELECT id, title, description, location, job_type, status, created_at FROM jobs WHERE id = $1" + ) + .bind(id) + .fetch_optional(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(job)) +} + +async fn health() -> &'static str { + "Jobs Service OK" +} + +#[tokio::main] +async fn main() { + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let database_url = std::env::var("DATABASE_URL") + .expect("DATABASE_URL must be set"); + + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(10) + .connect(&database_url) + .await + .expect("Failed to connect to database"); + + tracing::info!("Connected to database"); + + let state = Arc::new(AppState { pool }); + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + let app = Router::new() + .route("/health", get(health)) + .route("/jobs", get(list_jobs)) + .route("/jobs", post(create_job)) + .route("/jobs/:id", get(get_job)) + .layer(cors) + .with_state(state); + + let port: u16 = std::env::var("PORT") + .unwrap_or_else(|_| "9103".to_string()) + .parse() + .expect("PORT must be a valid u16"); + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + + tracing::info!("Jobs service listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/apps/leads/Cargo.toml b/apps/leads/Cargo.toml new file mode 100644 index 0000000..144c352 --- /dev/null +++ b/apps/leads/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "leads" +version = "0.1.0" +edition = "2021" + +[dependencies] +sqlx = { workspace = true } +axum = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +anyhow = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +tower-http = { version = "0.6", features = ["cors", "trace"] } + +[[bin]] +name = "leads" +path = "src/main.rs" diff --git a/apps/leads/src/main.rs b/apps/leads/src/main.rs new file mode 100644 index 0000000..9aac078 --- /dev/null +++ b/apps/leads/src/main.rs @@ -0,0 +1,136 @@ +use axum::{ + extract::State, + http::StatusCode, + routing::{get, post, put, delete}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use std::net::SocketAddr; +use std::sync::Arc; +use tower_http::cors::{Any, CorsLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[derive(Clone)] +pub struct AppState { + pub pool: PgPool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Lead { + pub id: uuid::Uuid, + pub title: String, + pub description: String, + pub location: String, + pub profession_key: String, + pub status: String, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateLead { + pub title: String, + pub description: String, + pub location: String, + pub profession_key: String, +} + +async fn list_leads(State(state): State>) -> Result>, StatusCode> { + let leads = sqlx::query_as::<_, Lead>( + "SELECT id, title, description, location, profession_key, status, created_at FROM requirements ORDER BY created_at DESC" + ) + .fetch_all(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(leads)) +} + +async fn create_lead( + State(state): State>, + Json(payload): Json, +) -> Result, StatusCode> { + let lead = sqlx::query_as::<_, Lead>( + r#" + INSERT INTO requirements (title, description, location, profession_key) + VALUES ($1, $2, $3, $4) + RETURNING id, title, description, location, profession_key, status, created_at + "#, + ) + .bind(&payload.title) + .bind(&payload.description) + .bind(&payload.location) + .bind(&payload.profession_key) + .fetch_one(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(lead)) +} + +async fn get_lead( + State(state): State>, + axum::extract::Path(id): axum::extract::Path, +) -> Result, StatusCode> { + let lead = sqlx::query_as::<_, Lead>( + "SELECT id, title, description, location, profession_key, status, created_at FROM requirements WHERE id = $1" + ) + .bind(id) + .fetch_optional(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(lead)) +} + +async fn health() -> &'static str { + "Leads Service OK" +} + +#[tokio::main] +async fn main() { + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let database_url = std::env::var("DATABASE_URL") + .expect("DATABASE_URL must be set"); + + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(10) + .connect(&database_url) + .await + .expect("Failed to connect to database"); + + tracing::info!("Connected to database"); + + let state = Arc::new(AppState { pool }); + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + let app = Router::new() + .route("/health", get(health)) + .route("/leads", get(list_leads)) + .route("/leads", post(create_lead)) + .route("/leads/:id", get(get_lead)) + .layer(cors) + .with_state(state); + + let port: u16 = std::env::var("PORT") + .unwrap_or_else(|_| "9118".to_string()) + .parse() + .expect("PORT must be a valid u16"); + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + + tracing::info!("Leads service listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/docker-compose.yml b/docker-compose.yml index 906ff56..fb280e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,8 @@ services: ADMIN_URL: http://localhost:9202 USERS_SERVICE_URL: http://users:9101 COMPANIES_SERVICE_URL: http://companies:9102 + JOBS_SERVICE_URL: http://jobs:9103 + LEADS_SERVICE_URL: http://leads:9118 JOB_SEEKERS_SERVICE_URL: http://job-seekers:9104 CUSTOMERS_SERVICE_URL: http://customers:9105 EMPLOYEES_SERVICE_URL: http://employees:9106 @@ -71,6 +73,10 @@ services: condition: service_started companies: condition: service_started + jobs: + condition: service_started + leads: + condition: service_started job-seekers: condition: service_started customers: @@ -130,6 +136,26 @@ services: redis: condition: service_healthy + jobs: + platform: linux/amd64 + image: ghcr.io/traceworks2023/nxtgauge-rust-jobs:high-performance-latest + environment: + PORT: "9103" + DATABASE_URL: postgresql://nxtgauge:nxtgauge_dev@postgres:5432/nxtgauge_db + depends_on: + postgres: + condition: service_healthy + + leads: + platform: linux/amd64 + image: ghcr.io/traceworks2023/nxtgauge-rust-leads:high-performance-latest + environment: + PORT: "9118" + DATABASE_URL: postgresql://nxtgauge:nxtgauge_dev@postgres:5432/nxtgauge_db + depends_on: + postgres: + condition: service_healthy + job-seekers: platform: linux/amd64 image: ghcr.io/traceworks2023/nxtgauge-rust-job-seekers:high-performance-latest