Files
silo-server/internal/branding/branding.go
5afe56cfc0 feat(jellycompat): add runtime-managed Jellyfin Web compatibility (#77)
* feat(jellycompat): install web assets at runtime

* fix(jellycompat): recover stale web operation locks

* fix(jellycompat): harden web component management

* feat(admin): refine compat settings and restart status

* chore(dev): add hot-reload docker compose stack

* fix(dev): include npm in hot-reload backend

* feat(admin): refine Jellyfin compatibility settings

* feat(settings): improve jellyfin proxy summary

* feat(settings): improve jellyfin web controls

* fix(settings): update jellyfin web removal status

* fix(settings): enable jellyfin web after install

* feat(jellycompat): auto-select web ui version

* test(api): update rate limit handler setup

* feat(jellycompat): refine web ui install onboarding

* fix(jellycompat): address web ui install review issues

* fix(onboarding): mirror jellyfin api runtime status

* fix(admin): remove global restart banner

* fix(settings): gate restart required tracking

* fix(jellyfin): ignore live settings for restart status

* fix(jellyfin): avoid restart for live compat settings

* fix(subtitles): normalize AI language codes

* fix(catalog): support partial title search tokens

* feat(branding): add white-label customization

* Add push relay engineering plan

- Document relay API contracts, APNs/FCM behavior, auth, storage, and ops
- Capture implementation plan, provider references, decisions, and README

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
2026-06-15 09:34:08 -04:00

83 lines
3.6 KiB
Go

// Package branding is the single source of truth for server white-labeling:
// server name, custom logos (wordmark + mark), favicon, login background, and
// the derived browser tab title, web app manifest, and theme color.
//
// It is intentionally a leaf package (it depends only on imageutil and the
// s3client value type via small interfaces) so that both internal/server (which
// templates index.html and serves the manifest/favicon) and
// internal/api/handlers (which exposes the public read + admin upload endpoints)
// can depend on it without an import cycle.
package branding
import "errors"
// AssetKind identifies an uploadable branding image.
type AssetKind string
const (
// KindWordmark is the wide logo shown in the expanded sidebar.
KindWordmark AssetKind = "wordmark"
// KindMark is the square icon shown in the collapsed sidebar and PWA install.
KindMark AssetKind = "mark"
// KindFavicon is the browser tab icon. Served as-is (no WebP re-encode) so
// Safari and mobile browsers keep working.
KindFavicon AssetKind = "favicon"
// KindLoginBg is the background image for the auth pages.
KindLoginBg AssetKind = "login_bg"
)
// Scalar branding settings keys (stored in the server_settings table). Asset
// keys live on each assetSpec.
const (
KeyServerName = "branding.server_name"
KeyLoginSubtitle = "branding.login_subtitle"
KeyAccentColor = "branding.accent_color"
KeyDefaultTheme = "branding.default_theme"
)
// Defaults applied when a branding setting is unset. ServerName/LoginSubtitle
// mirror the frontend's hardcoded fallbacks so behavior is unchanged out of the
// box.
const (
DefaultServerName = "Silo"
DefaultLoginSubtitle = "Sign in with an existing account."
// DefaultThemeColor is used for the PWA manifest theme/background color when
// no accent color is configured.
DefaultThemeColor = "#0b0b0f"
)
// assetURLBase is the public, stable path prefix for serving branding assets.
// Assets are addressed by content ref (?v=<hash><ext>) for immutable caching.
const assetURLBase = "/api/v1/branding/assets/"
// AssetContentSecurityPolicy hardens every served branding asset response.
//
// SECURITY: the favicon accepts SVG, a valid favicon format that can embed
// <script> and on* handlers. Served on the app origin and navigated to
// directly, such an SVG would otherwise execute in the viewer's session
// (stored XSS) — X-Content-Type-Options alone does not stop a correctly-typed
// SVG document from running scripts. `sandbox` (no allow-tokens) forces an
// opaque origin with scripting disabled, and `default-src 'none'` blocks all
// fetches; the asset still renders fine as an <img>/<link rel="icon">
// subresource (where this policy does not apply). Harmless for raster images.
const AssetContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; sandbox"
// Errors returned by Service. Handlers map these to HTTP status codes.
var (
// ErrStorageUnavailable indicates S3 is not configured; branding image
// upload/serving is unavailable but text branding still works.
ErrStorageUnavailable = errors.New("branding: asset storage is not configured")
// ErrAssetNotConfigured indicates no custom asset of the requested kind is set.
ErrAssetNotConfigured = errors.New("branding: asset not configured")
// ErrInvalidKind indicates an unknown asset kind.
ErrInvalidKind = errors.New("branding: invalid asset kind")
// ErrUnsupportedImage indicates the uploaded file is not an accepted image type.
ErrUnsupportedImage = errors.New("branding: unsupported image type")
)
// IsValidKind reports whether s names a known asset kind.
func IsValidKind(s string) bool {
_, ok := assetSpecs[AssetKind(s)]
return ok
}