From 413fa8aa2ceb0cb1e5d87b59269e4fc52566feb9 Mon Sep 17 00:00:00 2001 From: DmitrL-dev <84296377+DmitrL-dev@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:46:59 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20POST=20/api/waitlist=20=E2=80=94=20back?= =?UTF-8?q?end=20endpoint=20for=20registration=20waitlist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server.go: route registration (public, rate-limited) - soc_handlers.go: handleWaitlist with email validation, input sanitization - service.go: AddWaitlistEntry with audit trail + structured logging - Frontend form at /register already submits to this endpoint --- internal/application/soc/service.go | 14 ++++++ internal/transport/http/server.go | 2 + internal/transport/http/soc_handlers.go | 63 +++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/internal/application/soc/service.go b/internal/application/soc/service.go index 3103c8d..0728085 100644 --- a/internal/application/soc/service.go +++ b/internal/application/soc/service.go @@ -1436,3 +1436,17 @@ func (s *Service) ImportIncidents(incidents []peer.SyncIncident) (int, error) { } return imported, nil } + +// AddWaitlistEntry records a waitlist registration interest. +// Currently logs to the audit trail — DB persistence added when registration opens. +func (s *Service) AddWaitlistEntry(email, company, useCase string) { + if s.logger != nil { + s.logger.Record(audit.ModuleSOC, "WAITLIST:NEW", + fmt.Sprintf("email=%s company=%s use_case=%s", email, company, useCase)) + } + slog.Info("waitlist entry recorded", + "email", email, + "company", company, + "use_case", useCase, + ) +} diff --git a/internal/transport/http/server.go b/internal/transport/http/server.go index 35322a9..996c777 100644 --- a/internal/transport/http/server.go +++ b/internal/transport/http/server.go @@ -264,6 +264,8 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("POST /api/v1/scan", s.handlePublicScan) // Usage endpoint — returns scan quota for caller mux.HandleFunc("GET /api/v1/usage", s.handleUsage) + // Waitlist endpoint — registration interest capture (no auth, rate-limited) + mux.HandleFunc("POST /api/waitlist", s.handleWaitlist) // pprof debug endpoints (§P4C) — gated behind EnablePprof() if s.pprofEnabled { diff --git a/internal/transport/http/soc_handlers.go b/internal/transport/http/soc_handlers.go index 66f7fcf..f1c0ffc 100644 --- a/internal/transport/http/soc_handlers.go +++ b/internal/transport/http/soc_handlers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "strconv" "time" @@ -1581,3 +1582,65 @@ func (s *Server) handleUsage(w http.ResponseWriter, r *http.Request) { info := s.usageTracker.GetUsage(userID, ip) writeJSON(w, http.StatusOK, info) } + +// handleWaitlist captures registration interest when signups are closed. +// POST /api/waitlist body: {"email": "user@corp.com", "company": "CorpX", "use_case": "LLM protection"} +// Public endpoint, no auth required. Rate-limited globally. +func (s *Server) handleWaitlist(w http.ResponseWriter, r *http.Request) { + limitBody(w, r) + defer r.Body.Close() + + var req struct { + Email string `json:"email"` + Company string `json:"company"` + UseCase string `json:"use_case"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid JSON") + return + } + + // Validate email + if req.Email == "" || len(req.Email) < 5 || len(req.Email) > 254 { + writeError(w, http.StatusBadRequest, "valid email is required") + return + } + // Basic email format check + hasAt := false + for _, c := range req.Email { + if c == '@' { + hasAt = true + break + } + } + if !hasAt { + writeError(w, http.StatusBadRequest, "valid email is required") + return + } + + // Sanitize + if len(req.Company) > 200 { + req.Company = req.Company[:200] + } + if len(req.UseCase) > 1000 { + req.UseCase = req.UseCase[:1000] + } + + // Log the waitlist entry (always — even if DB fails) + slog.Info("waitlist submission", + "email", req.Email, + "company", req.Company, + "use_case", req.UseCase, + "ip", r.RemoteAddr, + ) + + // Persist via SOC repo if available + if s.socSvc != nil { + s.socSvc.AddWaitlistEntry(req.Email, req.Company, req.UseCase) + } + + writeJSON(w, http.StatusOK, map[string]any{ + "status": "ok", + "message": "You've been added to the waitlist. We'll notify you when registration opens.", + }) +}