package api import ( "errors" "net/http" "github.com/google/uuid" "github.com/alchemistkay/guestguard/internal/domain" "github.com/alchemistkay/guestguard/internal/storage" ) // hostFromContext returns the authed user's id, or writes 401 and returns // false. Used by host-facing handlers as the first line in the function. func hostFromContext(w http.ResponseWriter, r *http.Request) (uuid.UUID, bool) { uid, ok := UserIDFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthenticated") return uuid.Nil, false } return uid, true } // requireEventOwner fetches the event and confirms the authed user owns it. // On mismatch (or missing event) it returns 404 — never 403 — so a cross- // tenant probe cannot tell the difference between "event doesn't exist" and // "exists but belongs to someone else". // // Pre-Block-C this checked the events.host_id column directly. Block C // preserves the same semantics for the owner-only handlers (events.Update, // events.Delete, collaborator management) — but for shared-role endpoints, // callers should use requireRole instead. func requireEventOwner( w http.ResponseWriter, r *http.Request, events *storage.EventRepo, eventID, hostID uuid.UUID, ) (*domain.Event, bool) { ev, err := events.GetForHost(r.Context(), eventID, hostID) if err != nil { if errors.Is(err, domain.ErrEventNotFound) { writeError(w, http.StatusNotFound, "event not found") return nil, false } writeError(w, http.StatusInternalServerError, "failed to load event") return nil, false } return ev, true } // requireRole is the Block C authz gate. It resolves the user's role on the // event and confirms it's at least `min`. Missing role → 404 (avoids leaking // existence to outsiders). Role-too-low → 403 (the user IS on the event, // just not privileged enough — that's safe to surface; the UI will hide the // action anyway). // // On success it returns the event and the user's role so the handler can // branch on owner-only behaviours without a second lookup. func requireRole( w http.ResponseWriter, r *http.Request, events *storage.EventRepo, collabs *storage.CollaboratorRepo, eventID, userID uuid.UUID, min domain.Role, ) (*domain.Event, domain.Role, bool) { role, ok, err := collabs.RoleFor(r.Context(), eventID, userID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to resolve role") return nil, "", false } if !ok { // Not a collaborator. Treat the same way the legacy host_id check // did: 404, no leak. writeError(w, http.StatusNotFound, "event not found") return nil, "", false } if !role.AtLeast(min) { writeError(w, http.StatusForbidden, "insufficient role for this action") return nil, "", false } ev, err := events.Get(r.Context(), eventID) if err != nil { if errors.Is(err, domain.ErrEventNotFound) { // Race: the event was deleted between the role lookup and now. writeError(w, http.StatusNotFound, "event not found") return nil, "", false } writeError(w, http.StatusInternalServerError, "failed to load event") return nil, "", false } return ev, role, true }