From 77c8330b11e9cd2ff646e8e42bf9922895a2e7e8 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Fri, 14 Aug 2026 03:46:28 +0530 Subject: [PATCH] feat(waitlist): POST /api/waitlist for the nxtgauge.com coming-soon form The coming-soon page's 'Notify Me' form (nxtgauge-gitops/coming-soon/ index.html) only console.logged the email - nothing was actually captured. Adds a public (no auth, same trust model as a newsletter signup) endpoint on the users service: - New waitlist_signups table (email unique, created_at) - no user_id/ FK since these are anonymous pre-launch signups, not necessarily existing accounts. - apps/users/src/handlers/waitlist.rs: POST / -> INSERT ... ON CONFLICT DO NOTHING (repeat signups from the same email are a no-op, not an error), loose email validation (has @ and a dotted domain - this is a marketing signup, not an account, so overly strict validation just loses real signups to minor typos). - Routed /api/waitlist through the gateway to the users service, alongside the other public routes (packages, kb, etc.) Applied migration to nxtgauge_test and prod. Co-Authored-By: Claude Sonnet 5 --- apps/gateway/src/main.rs | 1 + apps/users/src/handlers/mod.rs | 1 + apps/users/src/handlers/waitlist.rs | 50 +++++++++++++++++++ apps/users/src/main.rs | 2 + .../20260814040000_waitlist_signups.down.sql | 6 +++ .../20260814040000_waitlist_signups.up.sql | 15 ++++++ 6 files changed, 75 insertions(+) create mode 100644 apps/users/src/handlers/waitlist.rs create mode 100644 crates/db/migrations/20260814040000_waitlist_signups.down.sql create mode 100644 crates/db/migrations/20260814040000_waitlist_signups.up.sql diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index 9a05add..78432ac 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -97,6 +97,7 @@ impl Services { || path.starts_with("/api/packages") || path.starts_with("/api/support") || path.starts_with("/api/reviews") + || path.starts_with("/api/waitlist") || path.starts_with("/api/admin/roles") || path.starts_with("/api/admin/users") || path.starts_with("/api/admin/verifications") diff --git a/apps/users/src/handlers/mod.rs b/apps/users/src/handlers/mod.rs index 04b709a..e74ffe4 100644 --- a/apps/users/src/handlers/mod.rs +++ b/apps/users/src/handlers/mod.rs @@ -28,4 +28,5 @@ pub mod external_roles; pub mod verifications; pub mod profile; pub mod role_meta; +pub mod waitlist; pub mod settings; diff --git a/apps/users/src/handlers/waitlist.rs b/apps/users/src/handlers/waitlist.rs new file mode 100644 index 0000000..7727419 --- /dev/null +++ b/apps/users/src/handlers/waitlist.rs @@ -0,0 +1,50 @@ +//! Public email capture for the nxtgauge.com coming-soon page +//! (nxtgauge-gitops/coming-soon/index.html's "Notify Me" form). +//! Deliberately unauthenticated - anyone can submit an email, same trust +//! model as a newsletter signup form. + +use crate::AppState; +use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; +use serde::{Deserialize, Serialize}; + +pub fn router() -> Router { + Router::new().route("/", post(join_waitlist)) +} + +#[derive(Debug, Deserialize)] +struct JoinWaitlistRequest { + email: String, +} + +#[derive(Debug, Serialize)] +struct JoinWaitlistResponse { + joined: bool, +} + +async fn join_waitlist( + State(state): State, + Json(body): Json, +) -> Result, (StatusCode, String)> { + let email = body.email.trim().to_lowercase(); + + // Deliberately loose validation (just "has an @ and something on both + // sides") - this is a pre-launch marketing signup, not an account, and + // being too strict just loses real signups to typos in TLDs etc. + let valid = { + let mut parts = email.splitn(2, '@'); + matches!((parts.next(), parts.next()), (Some(local), Some(domain)) if !local.is_empty() && domain.contains('.')) + }; + if !valid || email.len() > 255 { + return Err((StatusCode::BAD_REQUEST, "Please enter a valid email address".to_string())); + } + + sqlx::query( + "INSERT INTO waitlist_signups (email) VALUES ($1) ON CONFLICT (email) DO NOTHING", + ) + .bind(&email) + .execute(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(JoinWaitlistResponse { joined: true })) +} diff --git a/apps/users/src/main.rs b/apps/users/src/main.rs index e839085..1a1bdcd 100644 --- a/apps/users/src/main.rs +++ b/apps/users/src/main.rs @@ -119,6 +119,8 @@ async fn main() { .nest("/api/admin/payment-gateway-config", handlers::payment_gateway::router()) // ── Tracecoin Packages (public) ─────────────────────────────────── .nest("/api/packages", handlers::pricing::public_packages_router()) + + .nest("/api/waitlist", handlers::waitlist::router()) // ── Tracecoin Packages & Reports (admin) ────────────────────────── .nest("/api/admin/tracecoin-packages", handlers::pricing::packages_router()) .nest("/api/admin/reports", handlers::pricing::reports_router()) diff --git a/crates/db/migrations/20260814040000_waitlist_signups.down.sql b/crates/db/migrations/20260814040000_waitlist_signups.down.sql new file mode 100644 index 0000000..e27aede --- /dev/null +++ b/crates/db/migrations/20260814040000_waitlist_signups.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +DROP INDEX IF EXISTS idx_waitlist_signups_created_at; +DROP TABLE IF EXISTS waitlist_signups; + +COMMIT; diff --git a/crates/db/migrations/20260814040000_waitlist_signups.up.sql b/crates/db/migrations/20260814040000_waitlist_signups.up.sql new file mode 100644 index 0000000..180fcb1 --- /dev/null +++ b/crates/db/migrations/20260814040000_waitlist_signups.up.sql @@ -0,0 +1,15 @@ +-- Email capture for the nxtgauge.com coming-soon page's "Notify Me" form +-- (apps/users/src/handlers/waitlist.rs). No user_id/FK - these are +-- anonymous pre-launch signups, not necessarily existing accounts. + +BEGIN; + +CREATE TABLE IF NOT EXISTS waitlist_signups ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_waitlist_signups_created_at ON waitlist_signups(created_at); + +COMMIT;