package httpapi import ( "net/http" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/auth" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/store" ) // accessDenial describes why a viewer may not see a gallery. Each mode has // a distinct message so the frontend can show a matching gate page (log in, // ask for the share link, buy a ticket). The messages are part of the API // contract with GalleryClient.tsx — change both together. type accessDenial struct { status int msg string } // authorize is the single access-control decision point (PLAN.md §7); it is // called from both the gallery-view handler and the file handler so every // byte served re-checks. func (s *Server) authorize(r *http.Request, g store.Gallery, user *auth.User, token string) *accessDenial { if user != nil && user.IsAdmin() { return nil } // A server-minted view token grants access to this one gallery until it // expires; it is only ever issued after a successful authorize, and it // is what lets requests (which cannot carry a Bearer header) // through for non-public galleries. if token != "" && verifyViewToken([]byte(s.cfg.ViewTokenSecret), g.ID, token) { return nil } switch g.Visibility { case store.VisibilityPublic: return nil case store.VisibilityLink: if token != "" && token == g.ShareToken { return nil } return &accessDenial{http.StatusForbidden, "This gallery needs its share link"} case store.VisibilityTicket: // The share token is honored as an escape hatch (e.g. attendee +1s // without accounts, at the admin's discretion). if token != "" && token == g.ShareToken { return nil } if user == nil { return &accessDenial{http.StatusUnauthorized, "Authentication required"} } if g.EventID == "" { return &accessDenial{http.StatusForbidden, "This gallery is only available to event attendees"} } ok, err := s.db.HasPaidTicket(r.Context(), user.ID, g.EventID) if err != nil || !ok { return &accessDenial{http.StatusForbidden, "This gallery is only available to event attendees"} } return nil default: // private return &accessDenial{http.StatusForbidden, "This gallery is private"} } }