diff --git a/config/config.go b/config/config.go index 3353c869..f00294e8 100644 --- a/config/config.go +++ b/config/config.go @@ -111,7 +111,7 @@ var ( CrossOriginOpenerPolicy: "", ExpectCTHeader: "", }, - ExtraCSS: []httpd.CustomCSS{}, + Branding: httpd.Branding{}, } defaultRateLimiter = common.RateLimiterConfig{ Average: 0, @@ -1294,23 +1294,78 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) { return result, isSet } -func getHTTPDExtraCSSFromEnv(idx int) []httpd.CustomCSS { - var css []httpd.CustomCSS +func getHTTPDUIBrandingFromEnv(prefix string) (httpd.UIBranding, bool) { + var result httpd.UIBranding + isSet := false - for subIdx := 0; subIdx < 10; subIdx++ { - var customCSS httpd.CustomCSS - - path, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__EXTRA_CSS__%v__PATH", idx, subIdx)) - if ok { - customCSS.Path = path - } - - if path != "" { - css = append(css, customCSS) - } + name, ok := os.LookupEnv(fmt.Sprintf("%s__NAME", prefix)) + if ok { + result.Name = name + isSet = true } - return css + shortName, ok := os.LookupEnv(fmt.Sprintf("%s__SHORT_NAME", prefix)) + if ok { + result.ShortName = shortName + isSet = true + } + + faviconPath, ok := os.LookupEnv(fmt.Sprintf("%s__FAVICON_PATH", prefix)) + if ok { + result.FaviconPath = faviconPath + isSet = true + } + + logoPath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGO_PATH", prefix)) + if ok { + result.LogoPath = logoPath + isSet = true + } + + loginImagePath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGIN_IMAGE_PATH", prefix)) + if ok { + result.LoginImagePath = loginImagePath + isSet = true + } + + disclaimerName, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_NAME", prefix)) + if ok { + result.DisclaimerName = disclaimerName + isSet = true + } + + disclaimerPath, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_PATH", prefix)) + if ok { + result.DisclaimerPath = disclaimerPath + isSet = true + } + + extraCSS, ok := lookupStringListFromEnv(fmt.Sprintf("%s__EXTRA_CSS", prefix)) + if ok { + result.ExtraCSS = extraCSS + isSet = true + } + + return result, isSet +} + +func getHTTPDBrandingFromEnv(idx int) (httpd.Branding, bool) { + var result httpd.Branding + isSet := false + + webAdmin, ok := getHTTPDUIBrandingFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__BRANDING__WEB_ADMIN", idx)) + if ok { + result.WebAdmin = webAdmin + isSet = true + } + + webClient, ok := getHTTPDUIBrandingFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__BRANDING__WEB_CLIENT", idx)) + if ok { + result.WebClient = webClient + isSet = true + } + + return result, isSet } func getHTTPDWebClientIntegrationsFromEnv(idx int) []httpd.WebClientIntegration { @@ -1372,10 +1427,9 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool { isSet = true } - extraCSS := getHTTPDExtraCSSFromEnv(idx) - if len(extraCSS) > 0 { - binding.ExtraCSS = extraCSS - isSet = true + brandingConf, ok := getHTTPDBrandingFromEnv(idx) + if ok { + binding.Branding = brandingConf } return isSet diff --git a/config/config_test.go b/config/config_test.go index 390c07ef..3b962f80 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -828,7 +828,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Setenv("SFTPGO_HTTPD__BINDINGS__1__PORT", "8000") os.Setenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS", "0") os.Setenv("SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL", " 1") - os.Setenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH", "") + os.Setenv("SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_ADMIN__NAME", "Web Admin") + os.Setenv("SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_CLIENT__SHORT_NAME", "WebClient") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS", "127.0.1.1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PORT", "9000") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0") @@ -869,6 +870,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER", `max-age=86400, enforce, report-uri="https://foo.example/report"`) os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH", "logo.png") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH", "login_image.png") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME", "disclaimer") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS", "1.css,2.css") t.Cleanup(func() { os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT") @@ -877,6 +884,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__PORT") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_ADMIN__NAME") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_CLIENT__SHORT_NAME") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PORT") @@ -918,6 +927,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS") }) configDir := ".." @@ -941,7 +956,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Equal(t, "127.0.0.1", bindings[1].Address) require.False(t, bindings[1].EnableHTTPS) require.Equal(t, 12, bindings[0].MinTLSVersion) - require.Len(t, bindings[0].ExtraCSS, 0) require.True(t, bindings[1].EnableWebAdmin) require.True(t, bindings[1].EnableWebClient) require.True(t, bindings[1].RenderOpenAPI) @@ -949,7 +963,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Equal(t, 1, bindings[1].HideLoginURL) require.Empty(t, bindings[1].OIDC.ClientID) require.False(t, bindings[1].Security.Enabled) - require.Len(t, bindings[1].ExtraCSS, 0) + require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name) + require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName) require.Equal(t, 9000, bindings[2].Port) require.Equal(t, "127.0.1.1", bindings[2].Address) require.True(t, bindings[2].EnableHTTPS) @@ -997,9 +1012,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy) require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy) require.Equal(t, `max-age=86400, enforce, report-uri="https://foo.example/report"`, bindings[2].Security.ExpectCTHeader) - require.Len(t, bindings[2].ExtraCSS, 2) - require.Equal(t, "path1", bindings[2].ExtraCSS[0].Path) - require.Equal(t, "path2", bindings[2].ExtraCSS[1].Path) + require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath) + require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath) + require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath) + require.Equal(t, "disclaimer", bindings[2].Branding.WebClient.DisclaimerName) + require.Equal(t, "disclaimer.html", bindings[2].Branding.WebAdmin.DisclaimerPath) + require.Len(t, bindings[2].Branding.WebClient.ExtraCSS, 2) + require.Equal(t, "1.css", bindings[2].Branding.WebClient.ExtraCSS[0]) + require.Equal(t, "2.css", bindings[2].Branding.WebClient.ExtraCSS[1]) } func TestHTTPClientCertificatesFromEnv(t *testing.T) { diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 570b7de8..e0485bbf 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -272,8 +272,15 @@ The configuration file contains the following sections: - `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank. - `cross_origin_opener_policy`, string. Allows to set the `Cross-Origin-Opener-Policy` header value. Default: blank. - `expect_ct_header`, string. Allows to set the `Expect-CT` header value. Default: blank. - - `extra_css`, list of structs. Defines additional CSS files. Each struct has the following fields: - - `path`, string. Path to the CSS file relative to `static_files_path`. For example, if you create a directory named `extra_css` inside the static dir and put the `my.css` file in it, you must set `/extra_css/my.css` as path. + - `branding`, struct. Defines the supported customizations to suit your brand. It contains the `web_admin` and `web_client` structs that define customizations for the WebAdmin and the WebClient UIs. Each customization struct contains the following fields: + - `name`, string. Defines the UI name + - `short_name`, string. Define the short name to show next to the logo image + - `favicon_path`, string. Path to the favicon relative to `static_files_path`. For example, if you create a directory named `branding` inside the static dir and put the `favicon.ico` file in it, you must set `/branding/favicon.ico` as path. + - `logo_path`, string. Path to your logo relative to `static_files_path`. The preferred image size is 256x256 pixel + - `login_image_path`, string. Path to a custom image to show on the login screen relative to `static_files_path`. The preferred image size is 900x900 pixel + - `disclaimer_name`, string. Name for your optional disclaimer + - `disclaimer_path`, string. Path to the HTML page with the disclaimer relative to `static_files_path` + - `extra_css`, list of strings. Defines the paths, relative to `static_files_path`, to additional CSS files - `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir - `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled - `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive diff --git a/httpd/httpd.go b/httpd/httpd.go index 218f40cd..6235e2f5 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -314,12 +314,56 @@ func (s *SecurityConf) getHTTPSProxyHeaders() map[string]string { return headers } -// CustomCSS defines the configuration for custom CSS -type CustomCSS struct { - // Path to the CSS file relative to "static_files_path". - // For example, if you create a directory named "extra_css" inside the static dir - // and put the "my.css" file in it, you must set "/extra_css/my.css" as path. - Path string `json:"path" mapstructure:"path"` +// UIBranding defines the supported customizations for the web UIs +type UIBranding struct { + // Name defines the text to show at the login page and as HTML title + Name string `json:"name" mapstructure:"name"` + // ShortName defines the name to show next to the logo image + ShortName string `json:"short_name" mapstructure:"short_name"` + // Path to your logo relative to "static_files_path". + // For example, if you create a directory named "branding" inside the static dir and + // put the "mylogo.png" file in it, you must set "/branding/mylogo.png" as logo path. + LogoPath string `json:"logo_path" mapstructure:"logo_path"` + // Path to the image to show on the login screen relative to "static_files_path" + LoginImagePath string `json:"login_image_path" mapstructure:"login_image_path"` + // Path to your favicon relative to "static_files_path" + FaviconPath string `json:"favicon_path" mapstructure:"favicon_path"` + // DisclaimerName defines the name for the link to your optional disclaimer + DisclaimerName string `json:"disclaimer_name" mapstructure:"disclaimer_name"` + // Path to the HTML page for your disclaimer relative to "static_files_path". + DisclaimerPath string `json:"disclaimer_path" mapstructure:"disclaimer_path"` + // Additional CSS file paths, relative to "static_files_path", to include + ExtraCSS []string `json:"extra_css" mapstructure:"extra_css"` +} + +func (b *UIBranding) check() { + if b.LogoPath != "" { + b.LogoPath = util.CleanPath(b.LogoPath) + } else { + b.LogoPath = "/img/logo.png" + } + if b.LoginImagePath != "" { + b.LoginImagePath = util.CleanPath(b.LoginImagePath) + } else { + b.LoginImagePath = "/img/login_image.png" + } + if b.FaviconPath != "" { + b.FaviconPath = util.CleanPath(b.FaviconPath) + } else { + b.FaviconPath = "/favicon.ico" + } + if b.DisclaimerPath != "" { + b.DisclaimerPath = util.CleanPath(b.DisclaimerPath) + } + for idx := range b.ExtraCSS { + b.ExtraCSS[idx] = util.CleanPath(b.ExtraCSS[idx]) + } +} + +// Branding defines the branding-related customizations supported +type Branding struct { + WebAdmin UIBranding `json:"web_admin" mapstructure:"web_admin"` + WebClient UIBranding `json:"web_client" mapstructure:"web_client"` } // WebClientIntegration defines the configuration for an external Web Client integration @@ -379,8 +423,8 @@ type Binding struct { OIDC OIDC `json:"oidc" mapstructure:"oidc"` // Security defines security headers to add to HTTP responses and allows to restrict allowed hosts Security SecurityConf `json:"security" mapstructure:"security"` - // Additional CSS - ExtraCSS []CustomCSS `json:"extra_css" mapstructure:"extra_css"` + // Branding defines customizations to suit your brand + Branding Branding `json:"branding" mapstructure:"branding"` allowHeadersFrom []func(net.IP) bool } @@ -394,14 +438,21 @@ func (b *Binding) checkWebClientIntegrations() { b.WebClientIntegrations = integrations } -func (b *Binding) checkExtraCSS() { - var extraCSS []CustomCSS - for _, css := range b.ExtraCSS { - extraCSS = append(extraCSS, CustomCSS{ - Path: path.Join("/", css.Path), - }) +func (b *Binding) checkBranding() { + b.Branding.WebAdmin.check() + b.Branding.WebClient.check() + if b.Branding.WebAdmin.Name == "" { + b.Branding.WebAdmin.Name = "SFTPGo WebAdmin" + } + if b.Branding.WebAdmin.ShortName == "" { + b.Branding.WebAdmin.ShortName = "WebAdmin" + } + if b.Branding.WebClient.Name == "" { + b.Branding.WebClient.Name = "SFTPGo WebClient" + } + if b.Branding.WebClient.ShortName == "" { + b.Branding.WebClient.ShortName = "WebClient" } - b.ExtraCSS = extraCSS } func (b *Binding) parseAllowedProxy() error { @@ -642,7 +693,7 @@ func (c *Conf) Initialize(configDir string) error { return err } binding.checkWebClientIntegrations() - binding.checkExtraCSS() + binding.checkBranding() binding.Security.updateProxyHeaders() go func(b Binding) { diff --git a/httpd/internal_test.go b/httpd/internal_test.go index d7e8b988..db0a7519 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -301,21 +301,31 @@ func TestShouldBind(t *testing.T) { } } -func TestExtraCSSValidation(t *testing.T) { +func TestBrandingValidation(t *testing.T) { b := Binding{ - ExtraCSS: []CustomCSS{ - { - Path: "path1", + Branding: Branding{ + WebAdmin: UIBranding{ + LogoPath: "path1", + LoginImagePath: "login1.png", }, - { - Path: "../path2", + WebClient: UIBranding{ + FaviconPath: "favicon1.ico", + DisclaimerPath: "../path2", + ExtraCSS: []string{"1.css"}, }, }, } - b.checkExtraCSS() - require.Len(t, b.ExtraCSS, 2) - assert.Equal(t, "/path1", b.ExtraCSS[0].Path) - assert.Equal(t, "/path2", b.ExtraCSS[1].Path) + b.checkBranding() + assert.Equal(t, "/favicon.ico", b.Branding.WebAdmin.FaviconPath) + assert.Equal(t, "/path1", b.Branding.WebAdmin.LogoPath) + assert.Equal(t, "/login1.png", b.Branding.WebAdmin.LoginImagePath) + assert.Len(t, b.Branding.WebAdmin.ExtraCSS, 0) + assert.Equal(t, "/favicon1.ico", b.Branding.WebClient.FaviconPath) + assert.Equal(t, "/path2", b.Branding.WebClient.DisclaimerPath) + assert.Equal(t, "/img/login_image.png", b.Branding.WebClient.LoginImagePath) + if assert.Len(t, b.Branding.WebClient.ExtraCSS, 1) { + assert.Equal(t, "/1.css", b.Branding.WebClient.ExtraCSS[0]) + } } func TestRedactedConf(t *testing.T) { diff --git a/httpd/server.go b/httpd/server.go index c428e866..ca410e51 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -140,7 +140,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error, ip str Error: error, CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } if s.binding.showAdminLoginURL() { data.AltLoginURL = webAdminLoginPath @@ -516,7 +516,7 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, error, ip stri Error: error, CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebAdmin, } if s.binding.showClientLoginURL() { data.AltLoginURL = webClientLoginPath diff --git a/httpd/web.go b/httpd/web.go index 99329181..b1aabad5 100644 --- a/httpd/web.go +++ b/httpd/web.go @@ -32,7 +32,7 @@ type loginPage struct { AltLoginURL string ForgotPwdURL string OpenIDLoginURL string - ExtraCSS []CustomCSS + Branding UIBranding } type twoFactorPage struct { @@ -42,7 +42,7 @@ type twoFactorPage struct { CSRFToken string StaticURL string RecoveryURL string - ExtraCSS []CustomCSS + Branding UIBranding } type forgotPwdPage struct { @@ -51,7 +51,7 @@ type forgotPwdPage struct { CSRFToken string StaticURL string Title string - ExtraCSS []CustomCSS + Branding UIBranding } type resetPwdPage struct { @@ -60,7 +60,7 @@ type resetPwdPage struct { CSRFToken string StaticURL string Title string - ExtraCSS []CustomCSS + Branding UIBranding } func getSliceFromDelimitedValues(values, delimiter string) []string { diff --git a/httpd/webadmin.go b/httpd/webadmin.go index dfb742ba..e0439ae9 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -131,7 +131,7 @@ type basePage struct { HasDefender bool HasExternalLogin bool LoggedAdmin *dataprovider.Admin - ExtraCSS []CustomCSS + Branding UIBranding } type usersPage struct { @@ -465,7 +465,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) HasDefender: common.Config.DefenderConfig.Enabled, HasExternalLogin: isLoggedInWithOIDC(r), CSRFToken: csrfToken, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebAdmin, } } @@ -518,7 +518,7 @@ func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, error, ip strin CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, Title: pageForgotPwdTitle, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebAdmin, } renderAdminTemplate(w, templateForgotPassword, data) } @@ -530,7 +530,7 @@ func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, error, ip string CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, Title: pageResetPwdTitle, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebAdmin, } renderAdminTemplate(w, templateResetPassword, data) } @@ -543,7 +543,7 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, error, ip strin CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, RecoveryURL: webAdminTwoFactorRecoveryPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebAdmin, } renderAdminTemplate(w, templateTwoFactor, data) } @@ -555,7 +555,7 @@ func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, error, Error: error, CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebAdmin, } renderAdminTemplate(w, templateTwoFactorRecovery, data) } diff --git a/httpd/webclient.go b/httpd/webclient.go index 7e1835a2..0239eb6b 100644 --- a/httpd/webclient.go +++ b/httpd/webclient.go @@ -98,7 +98,7 @@ type baseClientPage struct { CSRFToken string HasExternalLogin bool LoggedUser *dataprovider.User - ExtraCSS []CustomCSS + Branding UIBranding } type dirMapping struct { @@ -110,7 +110,7 @@ type viewPDFPage struct { Title string URL string StaticURL string - ExtraCSS []CustomCSS + Branding UIBranding } type editFilePage struct { @@ -339,7 +339,7 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re CSRFToken: csrfToken, HasExternalLogin: isLoggedInWithOIDC(r), LoggedUser: getUserFromToken(r), - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } } @@ -350,7 +350,7 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error, ip CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, Title: pageClientForgotPwdTitle, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateForgotPassword, data) } @@ -362,7 +362,7 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error, ip CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, Title: pageClientResetPwdTitle, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateResetPassword, data) } @@ -415,7 +415,7 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error, ip CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, RecoveryURL: webClientTwoFactorRecoveryPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateTwoFactor, data) } @@ -427,7 +427,7 @@ func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, e Error: error, CSRFToken: createCSRFToken(ip), StaticURL: webStaticFilesPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateTwoFactorRecovery, data) } @@ -1263,7 +1263,7 @@ func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request Title: path.Base(name), URL: fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)), StaticURL: webStaticFilesPath, - ExtraCSS: s.binding.ExtraCSS, + Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateClientViewPDF, data) } diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index f3d28346..118c56aa 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -7001,9 +7001,9 @@ func TestHashedPasswords(t *testing.T) { user.Password = clearPwd conn, client, err := getSftpClient(user, usePubKey) if assert.NoError(t, err, "unable to login with password %#v", pwd) { - defer conn.Close() - defer client.Close() assert.NoError(t, checkBasicSFTP(client)) + conn.Close() + client.Close() } user.Password = pwd conn, client, err = getSftpClient(user, usePubKey) @@ -7026,9 +7026,9 @@ func TestHashedPasswords(t *testing.T) { user.Password = clearPwd conn, client, err = getSftpClient(user, usePubKey) if assert.NoError(t, err, "unable to login with password %#v", pwd) { - defer conn.Close() - defer client.Close() assert.NoError(t, checkBasicSFTP(client)) + conn.Close() + client.Close() } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) diff --git a/sftpgo.json b/sftpgo.json index 934a3780..394c5f8c 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -258,7 +258,28 @@ "cross_origin_opener_policy": "", "expect_ct_header": "" }, - "extra_css": [] + "branding": { + "web_admin": { + "name": "", + "short_name": "", + "favicon_path": "", + "logo_path": "", + "login_image_path": "", + "disclaimer_name": "", + "disclaimer_path": "", + "extra_css": [] + }, + "web_client": { + "name": "", + "short_name": "", + "favicon_path": "", + "logo_path": "", + "login_image_path": "", + "disclaimer_name": "", + "disclaimer_path": "", + "extra_css": [] + } + } } ], "templates_path": "templates", diff --git a/static/css/sftpgo.css b/static/css/sftpgo.css deleted file mode 100644 index 453f0822..00000000 --- a/static/css/sftpgo.css +++ /dev/null @@ -1,10 +0,0 @@ -.bg-login-image { - background-image: url(/static/img/logo.png); - background-size: contain; - background-repeat: no-repeat; - padding: 1em; -} - -.row.login-image { - height: 200px; -} diff --git a/static/img/login_image.png b/static/img/login_image.png new file mode 100644 index 00000000..ffb3569e Binary files /dev/null and b/static/img/login_image.png differ diff --git a/static/img/logo.png b/static/img/logo.png index 0c320ff7..19500d19 100644 Binary files a/static/img/logo.png and b/static/img/logo.png differ diff --git a/templates/common/forgot-password.html b/templates/common/forgot-password.html index 9d69a64b..28044e43 100644 --- a/templates/common/forgot-password.html +++ b/templates/common/forgot-password.html @@ -9,9 +9,9 @@ - {{.Title}} + {{.Branding.Name}} - Forgot password - + @@ -70,8 +70,8 @@ } - {{range .ExtraCSS}} - + {{range .Branding.ExtraCSS}} + {{end}} diff --git a/templates/common/reset-password.html b/templates/common/reset-password.html index ef117950..0eebe5a1 100644 --- a/templates/common/reset-password.html +++ b/templates/common/reset-password.html @@ -9,9 +9,9 @@ - {{.Title}} + {{.Branding.Name}} - Reset password - + @@ -70,8 +70,8 @@ } - {{range .ExtraCSS}} - + {{range .Branding.ExtraCSS}} + {{end}} diff --git a/templates/webadmin/adminsetup.html b/templates/webadmin/adminsetup.html index b77b87ec..a436a58e 100644 --- a/templates/webadmin/adminsetup.html +++ b/templates/webadmin/adminsetup.html @@ -11,7 +11,7 @@ SFTPGo - Setup - + diff --git a/templates/webadmin/base.html b/templates/webadmin/base.html index 77fa149c..ffd399d8 100644 --- a/templates/webadmin/base.html +++ b/templates/webadmin/base.html @@ -10,9 +10,9 @@ - SFTPGo Admin - {{template "title" .}} + {{.Branding.Name}} - {{template "title" .}} - + @@ -53,8 +53,8 @@ {{block "extra_css" .}}{{end}} - {{range .ExtraCSS}} - + {{range .Branding.ExtraCSS}} + {{end}} @@ -69,12 +69,12 @@