feat(tenant-api): add desktop content asset endpoint scoped by tenant

Lets desktop clients fetch tenant images/articles assets via Bearer-auth
without requiring the public asset signature, so stale-signed assets
served from existing articles still load on the desktop side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 12:20:54 +08:00
parent 7ddd3d0c6c
commit acf7738bdd
4 changed files with 151 additions and 0 deletions
@@ -10,6 +10,7 @@ import (
"image/jpeg"
"image/png"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -42,6 +43,21 @@ func (h *AssetHandler) Serve(c *gin.Context) {
return
}
h.serveObject(c, objectKey)
}
func (h *AssetHandler) ServeDesktop(c *gin.Context) {
client := MustDesktopClient(c.Request.Context())
objectKey, ok := objectKeyFromAssetToken(c.Param("token"))
if !ok || !isDesktopClientAssetKey(objectKey, client.TenantID) {
c.AbortWithStatus(http.StatusNotFound)
return
}
h.serveObject(c, objectKey)
}
func (h *AssetHandler) serveObject(c *gin.Context, objectKey string) {
content, err := h.app.ObjectStorage.GetBytes(c.Request.Context(), objectKey)
if err != nil || len(content) == 0 {
c.AbortWithStatus(http.StatusNotFound)
@@ -121,3 +137,29 @@ func (h *AssetHandler) parseToken(token string) (string, bool) {
return objectKey, true
}
func objectKeyFromAssetToken(token string) (string, bool) {
parts := strings.Split(token, ".")
if len(parts) != 2 {
return "", false
}
objectKeyBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return "", false
}
objectKey := strings.TrimSpace(string(objectKeyBytes))
if objectKey == "" {
return "", false
}
return objectKey, true
}
func isDesktopClientAssetKey(objectKey string, tenantID int64) bool {
if tenantID <= 0 {
return false
}
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
return strings.HasPrefix(objectKey, tenantPrefix+"images/") ||
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
}