fix(storage): emit origin-relative object URLs

Build signed object URLs as path-only (/api/objects/...) instead of
prefixing APP_BASE_URL, so media loads from whatever host or reverse
proxy served the app. Remove the now-unused base URL from the signer and
drop the APP_BASE_URL / Server.BaseURL config.
This commit is contained in:
iu
2026-10-12 12:14:46 +03:00
parent 4d0e24347c
commit eef7c6c5d0
18 changed files with 206 additions and 39 deletions
+20 -20
View File
@@ -17,19 +17,18 @@ import (
// route. It is shared by every storage backend so delivery is identical: the
// client always talks to the backend, never to object storage directly.
type Signer struct {
secret []byte
baseURL string
ttl time.Duration
window time.Duration
now func() time.Time
secret []byte
ttl time.Duration
window time.Duration
now func() time.Time
}
// NewSigner builds a signer. secret signs the URLs (required); baseURL is the
// public base URL of the app; ttl limits URL lifetime (defaults to one hour);
// window quantizes the expiry so the same object yields an identical URL within
// one window (defaults to ten minutes, and is capped at ttl). A non-positive
// window keeps the URL stable for its whole lifetime.
func NewSigner(baseURL, secret string, ttl, window time.Duration) (*Signer, error) {
// NewSigner builds a signer. secret signs the URLs (required); ttl limits URL
// lifetime (defaults to one hour); window quantizes the expiry so the same
// object yields an identical URL within one window (defaults to ten minutes, and
// is capped at ttl). A non-positive window keeps the URL stable for its whole
// lifetime.
func NewSigner(secret string, ttl, window time.Duration) (*Signer, error) {
if strings.TrimSpace(secret) == "" {
return nil, errors.New("url secret is required")
}
@@ -43,23 +42,24 @@ func NewSigner(baseURL, secret string, ttl, window time.Duration) (*Signer, erro
window = ttl
}
return &Signer{
secret: []byte(secret),
baseURL: strings.TrimRight(baseURL, "/"),
ttl: ttl,
window: window,
now: time.Now,
secret: []byte(secret),
ttl: ttl,
window: window,
now: time.Now,
}, nil
}
// URL returns a signed app URL for the given opaque key. The expiry is
// quantized to the signing window so repeated calls within that window return
// the identical URL, letting clients reuse cached objects.
// URL returns a signed object URL for the given opaque key. The URL is
// origin-relative (it has no scheme or host) so the client fetches the blob from
// the same origin that served the app, regardless of the deployment host. The
// expiry is quantized to the signing window so repeated calls within that window
// return the identical URL, letting clients reuse cached objects.
func (s *Signer) URL(key string) string {
exp := s.expiry()
q := url.Values{}
q.Set("e", strconv.FormatInt(exp, 10))
q.Set("s", s.sign(key, exp))
return fmt.Sprintf("%s/api/objects/%s?%s", s.baseURL, escapeKey(key), q.Encode())
return fmt.Sprintf("/api/objects/%s?%s", escapeKey(key), q.Encode())
}
// expiry is the quantized Unix expiry shared by all URLs issued in the current