package api import ( "encoding/json" "errors" "net/http" "regexp" "strconv" "time" "github.com/google/uuid" "github.com/alchemistkay/guestguard/internal/domain" "github.com/alchemistkay/guestguard/internal/storage" ) type eventHandler struct { repo *storage.EventRepo } type createEventRequest struct { HostID string `json:"host_id"` Name string `json:"name"` Slug string `json:"slug"` EventDate time.Time `json:"event_date"` Venue string `json:"venue"` MaxCapacity int `json:"max_capacity"` Settings map[string]any `json:"settings"` Status string `json:"status"` } var slugRe = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) func (h *eventHandler) create(w http.ResponseWriter, r *http.Request) { var req createEventRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if req.Name == "" { writeError(w, http.StatusBadRequest, "name is required") return } if !slugRe.MatchString(req.Slug) { writeError(w, http.StatusBadRequest, "slug must be lowercase alphanumeric with hyphens") return } if req.EventDate.IsZero() { writeError(w, http.StatusBadRequest, "event_date is required") return } hostID, err := uuid.Parse(req.HostID) if err != nil { writeError(w, http.StatusBadRequest, "host_id must be a valid uuid") return } status := domain.EventStatus(req.Status) if status == "" { status = domain.EventStatusDraft } if !status.Valid() { writeError(w, http.StatusBadRequest, "invalid status") return } ev, err := h.repo.Create(r.Context(), storage.CreateEventParams{ HostID: hostID, Name: req.Name, Slug: req.Slug, EventDate: req.EventDate, Venue: req.Venue, MaxCapacity: req.MaxCapacity, Settings: req.Settings, Status: status, }) if err != nil { if errors.Is(err, domain.ErrSlugTaken) { writeError(w, http.StatusConflict, "slug already in use") return } writeError(w, http.StatusInternalServerError, "failed to create event") return } writeJSON(w, http.StatusCreated, ev) } func (h *eventHandler) get(w http.ResponseWriter, r *http.Request) { id, ok := parseIDParam(w, r, "id") if !ok { return } ev, err := h.repo.Get(r.Context(), id) if err != nil { if errors.Is(err, domain.ErrEventNotFound) { writeError(w, http.StatusNotFound, "event not found") return } writeError(w, http.StatusInternalServerError, "failed to load event") return } writeJSON(w, http.StatusOK, ev) } func (h *eventHandler) list(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() limit := atoiOr(q.Get("limit"), 50) offset := atoiOr(q.Get("offset"), 0) var hostID uuid.UUID if v := q.Get("host_id"); v != "" { parsed, err := uuid.Parse(v) if err != nil { writeError(w, http.StatusBadRequest, "host_id must be a valid uuid") return } hostID = parsed } events, err := h.repo.List(r.Context(), hostID, limit, offset) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list events") return } if events == nil { events = []*domain.Event{} } writeJSON(w, http.StatusOK, map[string]any{ "events": events, "limit": limit, "offset": offset, }) } type updateEventRequest struct { Name *string `json:"name"` Slug *string `json:"slug"` EventDate *time.Time `json:"event_date"` Venue *string `json:"venue"` MaxCapacity *int `json:"max_capacity"` Settings *map[string]any `json:"settings"` Status *string `json:"status"` } func (h *eventHandler) update(w http.ResponseWriter, r *http.Request) { id, ok := parseIDParam(w, r, "id") if !ok { return } var req updateEventRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } params := storage.UpdateEventParams{ Name: req.Name, EventDate: req.EventDate, Venue: req.Venue, MaxCapacity: req.MaxCapacity, Settings: req.Settings, } if req.Slug != nil { if !slugRe.MatchString(*req.Slug) { writeError(w, http.StatusBadRequest, "slug must be lowercase alphanumeric with hyphens") return } params.Slug = req.Slug } if req.Status != nil { s := domain.EventStatus(*req.Status) if !s.Valid() { writeError(w, http.StatusBadRequest, "invalid status") return } params.Status = &s } ev, err := h.repo.Update(r.Context(), id, params) if err != nil { switch { case errors.Is(err, domain.ErrEventNotFound): writeError(w, http.StatusNotFound, "event not found") case errors.Is(err, domain.ErrSlugTaken): writeError(w, http.StatusConflict, "slug already in use") default: writeError(w, http.StatusInternalServerError, "failed to update event") } return } writeJSON(w, http.StatusOK, ev) } func (h *eventHandler) delete(w http.ResponseWriter, r *http.Request) { id, ok := parseIDParam(w, r, "id") if !ok { return } if err := h.repo.Delete(r.Context(), id); err != nil { if errors.Is(err, domain.ErrEventNotFound) { writeError(w, http.StatusNotFound, "event not found") return } writeError(w, http.StatusInternalServerError, "failed to delete event") return } w.WriteHeader(http.StatusNoContent) } func parseIDParam(w http.ResponseWriter, r *http.Request, name string) (uuid.UUID, bool) { raw := r.PathValue(name) id, err := uuid.Parse(raw) if err != nil { writeError(w, http.StatusBadRequest, name+" must be a valid uuid") return uuid.Nil, false } return id, true } func atoiOr(s string, fallback int) int { if s == "" { return fallback } if n, err := strconv.Atoi(s); err == nil { return n } return fallback }