mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat(branding): white-label — public /branding, admin write, shell theming (#41)
app_settings branding.* keys (product name, logo/favicon URLs, accent color, docs/support links). Public GET /branding so login and shell theme pre-auth; admin writes via PUT /settings. useBranding() applies document.title, accent CSS var and favicon; BrandWordmark replaces hardcoded wordmarks; admin Platform settings gain a Branding form. MCP containr_branding_get + settings_update. Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
9408464f12
commit
b47ced0135
14 files changed
+408
-8
No files matched your search
@@ -258,6 +258,7 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
|
||||
public.POST("/auth/login", handleLogin)
|
||||
public.POST("/auth/register", handleRegister)
|
||||
public.GET("/maintenance", handleMaintenancePage)
|
||||
public.GET("/branding", handleGetBranding)
|
||||
|
||||
// Wake callback for sleep-mode placeholders. Unauthenticated but
|
||||
// non-destructive: it only reconciles the service back to running.
|
||||
|
||||
@@ -23,10 +23,49 @@ const (
|
||||
settingSignupEnabled = "signup_enabled"
|
||||
settingCloudflareTunnelToken = "cloudflare_tunnel_token"
|
||||
|
||||
// White-label branding keys — unset fields fall back to Containr
|
||||
// defaults. Self-hosters rebrand their instance via these.
|
||||
settingBrandProductName = "branding.product_name"
|
||||
settingBrandLogoURL = "branding.logo_url"
|
||||
settingBrandFaviconURL = "branding.favicon_url"
|
||||
settingBrandAccent = "branding.accent_color"
|
||||
settingBrandDocsURL = "branding.docs_url"
|
||||
settingBrandSupportURL = "branding.support_url"
|
||||
|
||||
cloudflaredContainerName = "containr-cloudflared"
|
||||
cloudflaredImage = "cloudflare/cloudflared:latest"
|
||||
)
|
||||
|
||||
// brandingConfig is the public white-label payload. Fields are always
|
||||
// populated — defaults keep the instance Containr-branded.
|
||||
type brandingConfig struct {
|
||||
ProductName string `json:"product_name"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
FaviconURL string `json:"favicon_url"`
|
||||
AccentColor string `json:"accent_color"`
|
||||
DocsURL string `json:"docs_url"`
|
||||
SupportURL string `json:"support_url"`
|
||||
}
|
||||
|
||||
func resolveBranding(db *database.DB) brandingConfig {
|
||||
return brandingConfig{
|
||||
ProductName: settingValue(db, settingBrandProductName, "BRAND_NAME", "Containr"),
|
||||
LogoURL: settingValue(db, settingBrandLogoURL, "BRAND_LOGO_URL", ""),
|
||||
FaviconURL: settingValue(db, settingBrandFaviconURL, "BRAND_FAVICON_URL", ""),
|
||||
AccentColor: settingValue(db, settingBrandAccent, "BRAND_ACCENT_COLOR", ""),
|
||||
DocsURL: settingValue(db, settingBrandDocsURL, "BRAND_DOCS_URL", ""),
|
||||
SupportURL: settingValue(db, settingBrandSupportURL, "BRAND_SUPPORT_URL", ""),
|
||||
}
|
||||
}
|
||||
|
||||
// GET /branding — public so the login page and shell can brand themselves
|
||||
// before a session exists.
|
||||
func handleGetBranding(c *gin.Context) {
|
||||
db, _ := c.Get("db")
|
||||
database, _ := db.(*database.DB)
|
||||
c.JSON(http.StatusOK, resolveBranding(database))
|
||||
}
|
||||
|
||||
// settingValue resolves a setting: app_settings row first, env fallback,
|
||||
// then the caller's default.
|
||||
func settingValue(db *database.DB, key, envKey, fallback string) string {
|
||||
@@ -110,6 +149,7 @@ func handleGetSettings(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"signup_enabled": signupEnabled(db),
|
||||
"cloudflare_tunnel": cloudflareTunnelState(c.Request.Context(), db, client),
|
||||
"branding": resolveBranding(db),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,6 +162,14 @@ func handleUpdateSettings(c *gin.Context) {
|
||||
var req struct {
|
||||
SignupEnabled *bool `json:"signup_enabled"`
|
||||
CloudflareTunnelToken *string `json:"cloudflare_tunnel_token"`
|
||||
Branding *struct {
|
||||
ProductName *string `json:"product_name"`
|
||||
LogoURL *string `json:"logo_url"`
|
||||
FaviconURL *string `json:"favicon_url"`
|
||||
AccentColor *string `json:"accent_color"`
|
||||
DocsURL *string `json:"docs_url"`
|
||||
SupportURL *string `json:"support_url"`
|
||||
} `json:"branding"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -158,6 +206,36 @@ func handleUpdateSettings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if req.Branding != nil {
|
||||
fields := []struct {
|
||||
key string
|
||||
value *string
|
||||
}{
|
||||
{settingBrandProductName, req.Branding.ProductName},
|
||||
{settingBrandLogoURL, req.Branding.LogoURL},
|
||||
{settingBrandFaviconURL, req.Branding.FaviconURL},
|
||||
{settingBrandAccent, req.Branding.AccentColor},
|
||||
{settingBrandDocsURL, req.Branding.DocsURL},
|
||||
{settingBrandSupportURL, req.Branding.SupportURL},
|
||||
}
|
||||
for _, f := range fields {
|
||||
if f.value == nil {
|
||||
continue
|
||||
}
|
||||
v := strings.TrimSpace(*f.value)
|
||||
var err error
|
||||
if v == "" {
|
||||
err = deleteSetting(db, f.key)
|
||||
} else {
|
||||
err = setSetting(db, f.key, v, false)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save branding"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleGetSettings(c)
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,9 @@ func init() {
|
||||
register(spec{"containr_banners_update", "Update an announcement banner (admin). Args: id plus fields to change.", "PATCH", "/admin/banners/{id}", []string{"id"}, []string{"title", "body", "level", "active", "dismissible", "starts_at", "ends_at"}, false})
|
||||
register(spec{"containr_banners_delete", "Delete an announcement banner (admin). Args: id. Requires confirm=true.", "DELETE", "/admin/banners/{id}", []string{"id"}, nil, true})
|
||||
|
||||
register(spec{"containr_branding_get", "Get instance white-label branding (product name, logo, accent, links).", "GET", "/branding", nil, nil, false})
|
||||
register(spec{"containr_settings_update", "Update platform settings (admin). Args: signup_enabled (bool), cloudflare_tunnel_token (string, empty clears), branding (object: product_name, logo_url, favicon_url, accent_color, docs_url, support_url — empty string resets a field).", "PUT", "/settings", nil, []string{"signup_enabled", "cloudflare_tunnel_token", "branding"}, false})
|
||||
|
||||
// Nodes / agent tokens (admin)
|
||||
register(spec{"containr_nodes_list", "List node agents (admin)", "GET", "/agents", nil, nil, false})
|
||||
register(spec{"containr_nodes_get", "Get a node agent (admin)", "GET", "/agents/{id}", []string{"id"}, nil, false})
|
||||
|
||||
Reference in new issue
Block a user