Add view tokens for non-public gallery images and per-mode gate pages.
<img> tags cannot send Authorization headers, so non-public gallery photos were invisible even to authorized viewers. The server now mints short-lived HMAC view tokens (gallery-scoped, hour-bucketed) and embeds them in every file URL for non-public galleries. Access denials return distinct 403 messages per visibility mode, and the frontend renders a matching gate page (private, link-only, ticket-holders, login prompt) with an inline login modal so visitors never leave the gallery page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
93476ac72a
commit
617c884012
@@ -7,11 +7,10 @@ import (
|
||||
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/store"
|
||||
)
|
||||
|
||||
// accessDenial describes why a viewer may not see a gallery. Non-public
|
||||
// galleries return 404 (not 403) so their existence is not probeable — the
|
||||
// exception is ticket mode, whose 401/403 lets the frontend prompt login
|
||||
// or explain the attendee requirement (its existence is already public via
|
||||
// the event).
|
||||
// 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
|
||||
@@ -24,6 +23,13 @@ func (s *Server) authorize(r *http.Request, g store.Gallery, user *auth.User, to
|
||||
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 <img> requests (which cannot carry a Bearer header)
|
||||
// through for non-public galleries.
|
||||
if token != "" && verifyViewToken([]byte(s.cfg.JWTSecret), g.ID, token) {
|
||||
return nil
|
||||
}
|
||||
switch g.Visibility {
|
||||
case store.VisibilityPublic:
|
||||
return nil
|
||||
@@ -31,7 +37,7 @@ func (s *Server) authorize(r *http.Request, g store.Gallery, user *auth.User, to
|
||||
if token != "" && token == g.ShareToken {
|
||||
return nil
|
||||
}
|
||||
return &accessDenial{http.StatusNotFound, "Gallery not found"}
|
||||
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).
|
||||
@@ -50,6 +56,6 @@ func (s *Server) authorize(r *http.Request, g store.Gallery, user *auth.User, to
|
||||
}
|
||||
return nil
|
||||
default: // private
|
||||
return &accessDenial{http.StatusNotFound, "Gallery not found"}
|
||||
return &accessDenial{http.StatusForbidden, "This gallery is private"}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user