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
@@ -0,0 +1,49 @@
|
||||
package httpapi
|
||||
|
||||
// View tokens solve a browser reality: the gallery JSON is fetched with an
|
||||
// Authorization header, but <img> tags cannot send headers, so image
|
||||
// requests for non-public galleries would arrive anonymous and be denied.
|
||||
// After a viewer passes the gallery access check, the server mints a
|
||||
// short-lived HMAC token scoped to that one gallery and appends it to the
|
||||
// file URLs it returns — the same idea as an S3 presigned URL. Possession
|
||||
// grants view access to that gallery only, until expiry.
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Expiries are aligned to hour buckets so repeated requests within the
|
||||
// same hour mint the *same* token — image URLs stay stable and the
|
||||
// browser cache keeps working across refetches/polls. A token minted at
|
||||
// time t expires between 1h and 2h later.
|
||||
const viewTokenBucket = int64(3600)
|
||||
|
||||
func mintViewToken(secret []byte, galleryID string) string {
|
||||
exp := (time.Now().Unix()/viewTokenBucket + 2) * viewTokenBucket
|
||||
return fmt.Sprintf("v1.%d.%s", exp, viewTokenSig(secret, galleryID, exp))
|
||||
}
|
||||
|
||||
func verifyViewToken(secret []byte, galleryID, token string) bool {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 || parts[0] != "v1" {
|
||||
return false
|
||||
}
|
||||
exp, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || time.Now().Unix() > exp {
|
||||
return false
|
||||
}
|
||||
expected := viewTokenSig(secret, galleryID, exp)
|
||||
return hmac.Equal([]byte(expected), []byte(parts[2]))
|
||||
}
|
||||
|
||||
func viewTokenSig(secret []byte, galleryID string, exp int64) string {
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
fmt.Fprintf(mac, "photos-view:%s:%d", galleryID, exp)
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
Reference in New Issue
Block a user