feat(orion): add rest api for waitlist

This commit is contained in:
talksik
2026-03-29 09:04:15 -07:00
parent c471c28bb0
commit acac407fd0
8 changed files with 631 additions and 2 deletions
+157 -1
View File
@@ -15,6 +15,7 @@ import (
"github.com/flowy-live/llink/internal/network"
"github.com/flowy-live/llink/internal/particle"
"github.com/flowy-live/llink/internal/utils"
"github.com/flowy-live/llink/internal/waitlist"
)
type Handler struct {
@@ -23,15 +24,17 @@ type Handler struct {
networkSvc network.Service
particleSvc particle.Service
depotSvc depot.Service
waitlistSvc waitlist.Service
}
func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc network.Service, particleSvc particle.Service, depotSvc depot.Service) *Handler {
func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc network.Service, particleSvc particle.Service, depotSvc depot.Service, waitlistSvc waitlist.Service) *Handler {
return &Handler{
authSvc: authSvc,
humanSvc: humanSvc,
networkSvc: networkSvc,
particleSvc: particleSvc,
depotSvc: depotSvc,
waitlistSvc: waitlistSvc,
}
}
@@ -774,6 +777,159 @@ func (h *Handler) ConfirmUpload(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(resp)
}
// ============================================================================
// Waitlist DTOs
// ============================================================================
type AddToWaitlistRequest struct {
Email string `json:"email"`
Metadata map[string]string `json:"metadata"`
}
type WaitlistEntryResponse struct {
Id int `json:"id"`
Email string `json:"email"`
Metadata map[string]string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
InvitedAt *time.Time `json:"invited_at,omitempty"`
}
type InviteWaitlistEntrantRequest struct {
Email string `json:"email"`
}
// ============================================================================
// Waitlist Handlers
// ============================================================================
// AddToWaitlist adds an email to the waitlist (public, no auth)
func (h *Handler) AddToWaitlist(w http.ResponseWriter, r *http.Request) {
var req AddToWaitlistRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if req.Email == "" {
http.Error(w, "email is required", http.StatusBadRequest)
return
}
err := h.waitlistSvc.AddToWaitlist(r.Context(), req.Email, req.Metadata)
if err != nil {
if errors.Is(err, waitlist.AlreadyInWaitlistError) {
http.Error(w, "already in the waitlist", http.StatusConflict)
return
}
slog.Error("failed to add to waitlist", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// GetWaitlist returns all waitlist entries (admin-only)
func (h *Handler) GetWaitlist(w http.ResponseWriter, r *http.Request) {
if !middleware.IsAdminFromContext(r.Context()) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
filterParam := r.URL.Query().Get("filter")
filter := waitlist.GetWaitlistFilterAll
switch filterParam {
case "invited":
filter = waitlist.GetWaitlistFilterInvitedOnly
case "uninvited":
filter = waitlist.GetWaitlistFilterUninvitedOnly
}
entries, err := h.waitlistSvc.GetWaitlist(r.Context(), filter)
if err != nil {
slog.Error("failed to get waitlist", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
resp := make([]WaitlistEntryResponse, 0, len(entries))
for _, e := range entries {
resp = append(resp, waitlistEntryToDTO(e))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// GetWaitlistEntry returns a single waitlist entry by email (admin-only)
func (h *Handler) GetWaitlistEntry(w http.ResponseWriter, r *http.Request) {
if !middleware.IsAdminFromContext(r.Context()) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
email := r.PathValue("email")
if email == "" {
http.Error(w, "email is required", http.StatusBadRequest)
return
}
entry, err := h.waitlistSvc.GetWaitlistEntryByEmail(r.Context(), email)
if err != nil {
if errors.Is(err, waitlist.EntryNotFoundError) {
http.Error(w, "entry not found", http.StatusNotFound)
return
}
slog.Error("failed to get waitlist entry", "error", err, "email", email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(waitlistEntryToDTO(entry))
}
// InviteWaitlistEntrant marks a waitlist entry as invited (admin-only)
func (h *Handler) InviteWaitlistEntrant(w http.ResponseWriter, r *http.Request) {
if !middleware.IsAdminFromContext(r.Context()) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
var req InviteWaitlistEntrantRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if req.Email == "" {
http.Error(w, "email is required", http.StatusBadRequest)
return
}
if err := h.waitlistSvc.MarkWaitlistEntryInvited(r.Context(), req.Email); err != nil {
if errors.Is(err, waitlist.EntryNotFoundError) {
http.Error(w, "entry not found", http.StatusNotFound)
return
}
slog.Error("failed to invite waitlist entrant", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func waitlistEntryToDTO(e *waitlist.WaitlistEntry) WaitlistEntryResponse {
return WaitlistEntryResponse{
Id: e.Id,
Email: e.Email,
Metadata: e.Metadata,
CreatedAt: e.CreatedAt,
InvitedAt: e.InvitedAt,
}
}
// ============================================================================
// Helper Functions
// ============================================================================