package httpapi // View tokens solve a browser reality: the gallery JSON is fetched with an // Authorization header, but 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)) }