diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index 78432ac..8ee6592 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -98,6 +98,7 @@ impl Services { || path.starts_with("/api/support") || path.starts_with("/api/reviews") || path.starts_with("/api/waitlist") + || path.starts_with("/api/admin/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/waitlist.rs b/apps/users/src/handlers/waitlist.rs index 7727419..a915bf2 100644 --- a/apps/users/src/handlers/waitlist.rs +++ b/apps/users/src/handlers/waitlist.rs @@ -4,13 +4,26 @@ //! model as a newsletter signup form. use crate::AppState; -use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + routing::{get, post}, + Json, Router, +}; +use contracts::auth_middleware::{require_admin, AuthUser}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; pub fn router() -> Router { Router::new().route("/", post(join_waitlist)) } +/// Mounted at /api/admin/waitlist by the gateway. +pub fn admin_router() -> Router { + Router::new().route("/", get(list_waitlist)) +} + #[derive(Debug, Deserialize)] struct JoinWaitlistRequest { email: String, @@ -48,3 +61,51 @@ async fn join_waitlist( Ok(Json(JoinWaitlistResponse { joined: true })) } + +#[derive(Debug, Deserialize)] +struct ListWaitlistQuery { + page: Option, + limit: Option, +} + +#[derive(Debug, FromRow, Serialize)] +struct WaitlistSignupRow { + email: String, + created_at: DateTime, +} + +#[derive(Debug, Serialize)] +struct ListWaitlistResponse { + signups: Vec, + total: i64, + page: i64, + limit: i64, +} + +async fn list_waitlist( + auth: AuthUser, + State(state): State, + Query(q): Query, +) -> Result, (StatusCode, String)> { + require_admin(&auth).map_err(|_| (StatusCode::FORBIDDEN, "Admin access required".to_string()))?; + + let limit = q.limit.unwrap_or(200).clamp(1, 1000); + let page = q.page.unwrap_or(1).max(1); + let offset = (page - 1) * limit; + + let signups = sqlx::query_as::<_, WaitlistSignupRow>( + "SELECT email, created_at FROM waitlist_signups ORDER BY created_at DESC LIMIT $1 OFFSET $2", + ) + .bind(limit) + .bind(offset) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + let total: i64 = sqlx::query_scalar("SELECT count(*) FROM waitlist_signups") + .fetch_one(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(ListWaitlistResponse { signups, total, page, limit })) +} diff --git a/apps/users/src/main.rs b/apps/users/src/main.rs index 1a1bdcd..79dbfd3 100644 --- a/apps/users/src/main.rs +++ b/apps/users/src/main.rs @@ -121,6 +121,7 @@ async fn main() { .nest("/api/packages", handlers::pricing::public_packages_router()) .nest("/api/waitlist", handlers::waitlist::router()) + .nest("/api/admin/waitlist", handlers::waitlist::admin_router()) // ── Tracecoin Packages & Reports (admin) ────────────────────────── .nest("/api/admin/tracecoin-packages", handlers::pricing::packages_router()) .nest("/api/admin/reports", handlers::pricing::reports_router())