feat(waitlist): POST /api/waitlist for the nxtgauge.com coming-soon form
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m28s
build-and-release / build (cron) (push) Successful in 1m51s
build-and-release / build (companies) (push) Successful in 2m7s
build-and-release / build (customers) (push) Successful in 2m54s
build-and-release / build (developers) (push) Successful in 1m39s
build-and-release / build (fitness-trainers) (push) Successful in 1m39s
build-and-release / build (gateway) (push) Successful in 1m31s
build-and-release / build (jobs) (push) Successful in 42s
build-and-release / build (employees) (push) Successful in 2m22s
build-and-release / build (graphic-designers) (push) Successful in 2m8s
build-and-release / build (makeup-artists) (push) Successful in 1m58s
build-and-release / build (payments) (push) Successful in 1m48s
build-and-release / build (job-seekers) (push) Successful in 3m4s
build-and-release / build (social-media-managers) (push) Successful in 2m39s
build-and-release / build (photographers) (push) Successful in 2m42s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (tutors) (push) Successful in 3m4s
build-and-release / build (ugc-content-creators) (push) Successful in 2m44s
build-and-release / build (video-editors) (push) Successful in 2m43s
build-and-release / build (users) (push) Successful in 4m38s
All checks were successful
build-and-release / build (catering-services) (push) Successful in 1m28s
build-and-release / build (cron) (push) Successful in 1m51s
build-and-release / build (companies) (push) Successful in 2m7s
build-and-release / build (customers) (push) Successful in 2m54s
build-and-release / build (developers) (push) Successful in 1m39s
build-and-release / build (fitness-trainers) (push) Successful in 1m39s
build-and-release / build (gateway) (push) Successful in 1m31s
build-and-release / build (jobs) (push) Successful in 42s
build-and-release / build (employees) (push) Successful in 2m22s
build-and-release / build (graphic-designers) (push) Successful in 2m8s
build-and-release / build (makeup-artists) (push) Successful in 1m58s
build-and-release / build (payments) (push) Successful in 1m48s
build-and-release / build (job-seekers) (push) Successful in 3m4s
build-and-release / build (social-media-managers) (push) Successful in 2m39s
build-and-release / build (photographers) (push) Successful in 2m42s
backend-integration-tests / ai-credits (push) Successful in 50s
build-and-release / build (tutors) (push) Successful in 3m4s
build-and-release / build (ugc-content-creators) (push) Successful in 2m44s
build-and-release / build (video-editors) (push) Successful in 2m43s
build-and-release / build (users) (push) Successful in 4m38s
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 <noreply@anthropic.com>
This commit is contained in:
parent
c2fb7d61f7
commit
77c8330b11
6 changed files with 75 additions and 0 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -28,4 +28,5 @@ pub mod external_roles;
|
|||
pub mod verifications;
|
||||
pub mod profile;
|
||||
pub mod role_meta;
|
||||
pub mod waitlist;
|
||||
pub mod settings;
|
||||
|
|
|
|||
50
apps/users/src/handlers/waitlist.rs
Normal file
50
apps/users/src/handlers/waitlist.rs
Normal file
|
|
@ -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<AppState> {
|
||||
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<AppState>,
|
||||
Json(body): Json<JoinWaitlistRequest>,
|
||||
) -> Result<Json<JoinWaitlistResponse>, (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 }))
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS idx_waitlist_signups_created_at;
|
||||
DROP TABLE IF EXISTS waitlist_signups;
|
||||
|
||||
COMMIT;
|
||||
15
crates/db/migrations/20260814040000_waitlist_signups.up.sql
Normal file
15
crates/db/migrations/20260814040000_waitlist_signups.up.sql
Normal file
|
|
@ -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;
|
||||
Loading…
Add table
Reference in a new issue