diff --git a/deploy/brand/apple-touch-icon.png b/deploy/brand/apple-touch-icon.png new file mode 100644 index 0000000..dc6cc30 Binary files /dev/null and b/deploy/brand/apple-touch-icon.png differ diff --git a/deploy/brand/favicon-16.png b/deploy/brand/favicon-16.png new file mode 100644 index 0000000..eb50ee9 Binary files /dev/null and b/deploy/brand/favicon-16.png differ diff --git a/deploy/brand/favicon-32.png b/deploy/brand/favicon-32.png new file mode 100644 index 0000000..d6ddbb7 Binary files /dev/null and b/deploy/brand/favicon-32.png differ diff --git a/deploy/brand/favicon.ico b/deploy/brand/favicon.ico new file mode 100644 index 0000000..98e3cc7 Binary files /dev/null and b/deploy/brand/favicon.ico differ diff --git a/deploy/brand/logo-256.png b/deploy/brand/logo-256.png new file mode 100644 index 0000000..7fcc125 Binary files /dev/null and b/deploy/brand/logo-256.png differ diff --git a/deploy/brand/logo.png b/deploy/brand/logo.png new file mode 100644 index 0000000..7fcc125 Binary files /dev/null and b/deploy/brand/logo.png differ diff --git a/deploy/nginx/host/sparkapi.conf b/deploy/nginx/host/sparkapi.conf index ccc37f4..75b0fac 100644 --- a/deploy/nginx/host/sparkapi.conf +++ b/deploy/nginx/host/sparkapi.conf @@ -67,11 +67,33 @@ server { proxy_set_header X-Forwarded-Proto https; } + # Brand icons (no app rebuild required) + location = /favicon.ico { + alias /var/www/sparkapi-brand/favicon.ico; + add_header Cache-Control "public, max-age=604800"; + access_log off; + } + location = /apple-touch-icon.png { + alias /var/www/sparkapi-brand/apple-touch-icon.png; + add_header Cache-Control "public, max-age=604800"; + access_log off; + } + location ^~ /brand/ { + alias /var/www/sparkapi-brand/; + add_header Cache-Control "public, max-age=604800"; + access_log off; + } + location / { proxy_pass http://spark_http; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; + # Inject favicon + header logo until next spark-server image ships docs HTML. + sub_filter_once off; + sub_filter_types text/html; + sub_filter '' ''; + sub_filter 'Spark IM' 'Spark IM'; } } diff --git a/internal/transport/http/auth.go b/internal/transport/http/auth.go index 25fcc21..d399084 100644 --- a/internal/transport/http/auth.go +++ b/internal/transport/http/auth.go @@ -86,7 +86,8 @@ func authLimitPath(path string) bool { func publicPath(path string) bool { switch path { - case "/healthz", "/readyz", "/openapi.yaml", "/docs", "/docs/": + case "/healthz", "/readyz", "/openapi.yaml", "/docs", "/docs/", + "/favicon.ico", "/apple-touch-icon.png": return true case "/v1/meta/enums", "/v1/auth/login", "/v1/auth/token/refresh", "/v1/sms/codes", "/v1/music/playlist", "/v1/music/settings": diff --git a/internal/transport/http/docs.go b/internal/transport/http/docs.go index e8322d1..b336d0a 100644 --- a/internal/transport/http/docs.go +++ b/internal/transport/http/docs.go @@ -1,7 +1,10 @@ package httpx import ( + "io/fs" "net/http" + "path" + "strings" "github.com/gin-gonic/gin" ) @@ -19,4 +22,52 @@ func registerDocs(r *gin.Engine) { r.GET("/docs/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/docs") }) + + staticRoot, err := fs.Sub(docsStatic, "docs/static") + if err != nil { + panic("docs static embed: " + err.Error()) + } + serveStaticFile := func(name, contentType string) gin.HandlerFunc { + return func(c *gin.Context) { + b, err := fs.ReadFile(staticRoot, name) + if err != nil { + c.Status(http.StatusNotFound) + return + } + c.Header("Cache-Control", "public, max-age=604800") + c.Data(http.StatusOK, contentType, b) + } + } + r.GET("/favicon.ico", serveStaticFile("favicon.ico", "image/x-icon")) + r.GET("/apple-touch-icon.png", serveStaticFile("apple-touch-icon.png", "image/png")) + r.GET("/docs/static/*filepath", func(c *gin.Context) { + rel := strings.TrimPrefix(c.Param("filepath"), "/") + rel = path.Clean("/" + rel) + rel = strings.TrimPrefix(rel, "/") + if rel == "" || strings.Contains(rel, "..") { + c.Status(http.StatusNotFound) + return + } + b, err := fs.ReadFile(staticRoot, rel) + if err != nil { + c.Status(http.StatusNotFound) + return + } + ct := contentTypeFor(rel) + c.Header("Cache-Control", "public, max-age=604800") + c.Data(http.StatusOK, ct, b) + }) +} + +func contentTypeFor(name string) string { + switch strings.ToLower(path.Ext(name)) { + case ".png": + return "image/png" + case ".ico": + return "image/x-icon" + case ".svg": + return "image/svg+xml" + default: + return "application/octet-stream" + } } diff --git a/internal/transport/http/docs/index.html b/internal/transport/http/docs/index.html index ba66521..af328d6 100644 --- a/internal/transport/http/docs/index.html +++ b/internal/transport/http/docs/index.html @@ -3,6 +3,9 @@ Spark IM API + + +