Skip to main content

mockforge_registry_server/handlers/
waitlist.rs

1//! Waitlist / beta signup handlers
2
3use axum::{
4    extract::{Query, State},
5    Json,
6};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9use validator::Validate;
10
11use crate::{
12    error::{ApiError, ApiResult},
13    AppState,
14};
15
16#[derive(Debug, Deserialize, Validate)]
17pub struct SubscribeRequest {
18    #[validate(email(message = "Invalid email address"))]
19    pub email: String,
20    /// Where the signup came from (e.g. "landing_page", "pricing_page", "blog")
21    pub source: Option<String>,
22}
23
24#[derive(Debug, Serialize)]
25pub struct SubscribeResponse {
26    pub success: bool,
27    pub message: String,
28}
29
30#[derive(Debug, Deserialize)]
31pub struct UnsubscribeQuery {
32    pub token: Uuid,
33}
34
35/// POST /api/v1/waitlist/subscribe — public, rate-limited
36pub async fn subscribe(
37    State(state): State<AppState>,
38    Json(request): Json<SubscribeRequest>,
39) -> ApiResult<Json<SubscribeResponse>> {
40    request.validate().map_err(|e| ApiError::InvalidRequest(e.to_string()))?;
41
42    let email = request.email.trim().to_lowercase();
43    let source = request.source.as_deref().unwrap_or("landing_page");
44
45    state.store.subscribe_waitlist(&email, source).await?;
46
47    tracing::info!(email = %email, source = %source, "Waitlist subscriber added");
48
49    Ok(Json(SubscribeResponse {
50        success: true,
51        message: "You're on the list! We'll notify you when we launch.".to_string(),
52    }))
53}
54
55/// GET /api/v1/waitlist/unsubscribe?token=<uuid> — public
56pub async fn unsubscribe(
57    State(state): State<AppState>,
58    Query(query): Query<UnsubscribeQuery>,
59) -> ApiResult<Json<SubscribeResponse>> {
60    let removed = state.store.unsubscribe_waitlist_by_token(query.token).await?;
61
62    if removed {
63        Ok(Json(SubscribeResponse {
64            success: true,
65            message: "You've been unsubscribed.".to_string(),
66        }))
67    } else {
68        Ok(Json(SubscribeResponse {
69            success: false,
70            message: "Link is invalid or you're already unsubscribed.".to_string(),
71        }))
72    }
73}