浏览代码

web UIs: add branding support

Fixes #829

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 年之前
父节点
当前提交
5d7f6960f3

+ 71 - 17
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
+	name, ok := os.LookupEnv(fmt.Sprintf("%s__NAME", prefix))
+	if ok {
+		result.Name = name
+		isSet = true
+	}
 
-		path, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__EXTRA_CSS__%v__PATH", idx, subIdx))
-		if ok {
-			customCSS.Path = path
-		}
+	shortName, ok := os.LookupEnv(fmt.Sprintf("%s__SHORT_NAME", prefix))
+	if ok {
+		result.ShortName = shortName
+		isSet = true
+	}
 
-		if path != "" {
-			css = append(css, customCSS)
-		}
+	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
 	}
 
-	return css
+	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

+ 26 - 6
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) {

+ 9 - 2
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

+ 67 - 16
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) {

+ 20 - 10
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) {

+ 2 - 2
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

+ 4 - 4
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 {

+ 6 - 6
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)
 }

+ 8 - 8
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)
 }

+ 4 - 4
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)

+ 22 - 1
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",

+ 0 - 10
static/css/sftpgo.css

@@ -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;
-}

二进制
static/img/login_image.png


二进制
static/img/logo.png


+ 4 - 4
templates/common/forgot-password.html

@@ -9,9 +9,9 @@
     <meta name="description" content="">
     <meta name="author" content="">
 
-    <title>{{.Title}}</title>
+    <title>{{.Branding.Name}} - Forgot password</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom styles for this template-->
     <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">
@@ -70,8 +70,8 @@
         }
     </style>
 
-    {{range .ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{range .Branding.ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
     {{end}}
 
 </head>

+ 4 - 4
templates/common/reset-password.html

@@ -9,9 +9,9 @@
     <meta name="description" content="">
     <meta name="author" content="">
 
-    <title>{{.Title}}</title>
+    <title>{{.Branding.Name}} - Reset password</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom styles for this template-->
     <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">
@@ -70,8 +70,8 @@
         }
     </style>
 
-    {{range .ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{range .Branding.ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
     {{end}}
 
 </head>

+ 1 - 1
templates/webadmin/adminsetup.html

@@ -11,7 +11,7 @@
 
     <title>SFTPGo - Setup</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom styles for this template-->
     <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">

+ 8 - 8
templates/webadmin/base.html

@@ -10,9 +10,9 @@
     <meta name="description" content="">
     <meta name="author" content="">
 
-    <title>SFTPGo Admin - {{template "title" .}}</title>
+    <title>{{.Branding.Name}} - {{template "title" .}}</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom fonts for this template-->
     <link href="{{.StaticURL}}/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
@@ -53,8 +53,8 @@
     </style>
     {{block "extra_css" .}}{{end}}
 
-    {{range .ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{range .Branding.ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
     {{end}}
 
 </head>
@@ -69,12 +69,12 @@
         <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
 
             <!-- Sidebar - Brand -->
-            <a class="sidebar-brand d-flex align-items-center justify-content-center" href="{{.UsersURL}}">
+            <div class="sidebar-brand d-flex align-items-center justify-content-center">
                 <div class="sidebar-brand-icon">
-                    <i class="fas fa-folder-open"></i>
+                    <img src="{{.StaticURL}}{{.Branding.LogoPath}}" alt="logo" style="width: 2rem; height: auto;">
                 </div>
-                <div class="sidebar-brand-text mx-3" style="text-transform: none;">SFTPGo Admin</div>
-            </a>
+                <div class="sidebar-brand-text mx-3" style="text-transform: none;">{{.Branding.ShortName}}</div>
+            </div>
 
             <!-- Divider -->
             <hr class="sidebar-divider my-0">

+ 17 - 6
templates/webadmin/baselogin.html

@@ -10,9 +10,9 @@
     <meta name="description" content="">
     <meta name="author" content="">
 
-    <title>SFTPGo Admin - {{template "title" .}}</title>
+    <title>{{.Branding.Name}} - {{template "title" .}}</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom styles for this template-->
     <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">
@@ -70,10 +70,21 @@
             border-radius: 10rem;
             padding: 0.75rem 1rem;
         }
+
+        .bg-login-image {
+            background-image: url('{{.StaticURL}}{{.Branding.LoginImagePath}}');
+            background-size: contain;
+            background-repeat: no-repeat;
+            padding: 1em;
+        }
+
+        .row.login-image {
+            height: 200px;
+        }
     </style>
 
-    {{range .ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{range .Branding.ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
     {{end}}
 
 </head>
@@ -95,9 +106,9 @@
                             </div>
                         </div>
                         <div class="row">
-                            <div class="col-lg-6 d-none d-lg-block bg-login-image">
+                            <div class="col-lg-5 d-none d-lg-block bg-login-image">
                             </div>
-                            <div class="col-lg-6">
+                            <div class="col-lg-7">
                                 <div class="p-5">
                                     {{template "content" .}}
                                 </div>

+ 7 - 1
templates/webadmin/login.html

@@ -4,7 +4,7 @@
 
 {{define "content"}}
                                     <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">SFTPGo Admin - {{.Version}}</h1>
+                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}} - {{.Version}}</h1>
                                     </div>
                                     {{if .Error}}
                                     <div class="card mb-4 border-left-warning">
@@ -43,4 +43,10 @@
                                         <a class="small" href="{{.AltLoginURL}}">Web Client</a>
                                     </div>
                                     {{end}}
+                                    {{if and .Branding.DisclaimerName .Branding.DisclaimerPath}}
+                                    <hr>
+                                    <div class="text-center">
+                                        <a class="small" href="{{.Branding.DisclaimerPath}}" target="_blank">{{.Branding.DisclaimerName}}</a>
+                                    </div>
+                                    {{end}}
 {{end}}

+ 1 - 1
templates/webadmin/twofactor-recovery.html

@@ -4,7 +4,7 @@
 
 {{define "content"}}
                                     <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">SFTPGo Admin - {{.Version}}</h1>
+                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}} - {{.Version}}</h1>
                                     </div>
                                     {{if .Error}}
                                     <div class="card mb-4 border-left-warning">

+ 1 - 1
templates/webadmin/twofactor.html

@@ -4,7 +4,7 @@
 
 {{define "content"}}
                                     <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">SFTPGo Admin - {{.Version}}</h1>
+                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}} - {{.Version}}</h1>
                                     </div>
                                     {{if .Error}}
                                     <div class="card mb-4 border-left-warning">

+ 8 - 5
templates/webclient/base.html

@@ -10,9 +10,9 @@
     <meta name="description" content="">
     <meta name="author" content="">
 
-    <title>SFTPGo WebClient - {{template "title" .}}</title>
+    <title>{{.Branding.Name}} - {{template "title" .}}</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom fonts for this template-->
     <link href="{{.StaticURL}}/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
@@ -53,8 +53,8 @@
     </style>
     {{block "extra_css" .}}{{end}}
 
-    {{range .ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{range .Branding.ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
     {{end}}
 
 </head>
@@ -70,7 +70,10 @@
 
             <!-- Sidebar - Brand -->
             <div class="sidebar-brand d-flex align-items-center justify-content-center">
-                <div style="text-transform: none;">SFTPGo WebClient</div>
+                <div class="sidebar-brand-icon">
+                    <img src="{{.StaticURL}}{{.Branding.LogoPath}}" alt="logo" style="width: 2rem; height: auto;">
+                </div>
+                <div class="sidebar-brand-text mx-3" style="text-transform: none;">{{.Branding.ShortName}}</div>
             </div>
 
             <!-- Divider -->

+ 18 - 7
templates/webclient/baselogin.html

@@ -10,9 +10,9 @@
     <meta name="description" content="">
     <meta name="author" content="">
 
-    <title>SFTPGo WebClient - {{template "title" .}}</title>
+    <title>{{.Branding.Name}} - {{template "title" .}}</title>
 
-    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
 
     <!-- Custom styles for this template-->
     <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">
@@ -70,10 +70,21 @@
             border-radius: 10rem;
             padding: 0.75rem 1rem;
         }
+
+        .bg-login-image {
+            background-image: url('{{.StaticURL}}{{.Branding.LoginImagePath}}');
+            background-size: contain;
+            background-repeat: no-repeat;
+            padding: 1em;
+        }
+
+        .row.login-image {
+            height: 200px;
+        }
     </style>
 
-    {{range .ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{range .Branding.ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
     {{end}}
 
 </head>
@@ -95,12 +106,12 @@
                             </div>
                         </div>
                         <div class="row">
-                            <div class="col-lg-6 d-none d-lg-block bg-login-image">
+                            <div class="col-lg-5 d-none d-lg-block bg-login-image">
                             </div>
-                            <div class="col-lg-6">
+                            <div class="col-lg-7">
                                 <div class="p-5">
                                     <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">SFTPGo WebClient - {{.Version}}</h1>
+                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}} - {{.Version}}</h1>
                                     </div>
                                     {{template "content" .}}
                                 </div>

+ 6 - 0
templates/webclient/login.html

@@ -40,4 +40,10 @@
                                         <a class="small" href="{{.AltLoginURL}}">Web Admin</a>
                                     </div>
                                     {{end}}
+                                    {{if and .Branding.DisclaimerName .Branding.DisclaimerPath}}
+                                    <hr>
+                                    <div class="text-center">
+                                        <a class="small" href="{{.Branding.DisclaimerPath}}" target="_blank">{{.Branding.DisclaimerName}}</a>
+                                    </div>
+                                    {{end}}
 {{end}}

+ 2 - 2
templates/webclient/viewpdf.html

@@ -5,8 +5,8 @@
         <meta name="viewport" content="width=device-width, initial-scale=1" />
         <title>{{.Title}}</title>
 
-        {{range .ExtraCSS}}
-        <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+        {{range .Branding.ExtraCSS}}
+        <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
         {{end}}
     </head>