Sfoglia il codice sorgente

web UI: allow to load custom css

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 anni fa
parent
commit
93b9c1617e

+ 51 - 15
config/config.go

@@ -108,6 +108,7 @@ var (
 			CrossOriginOpenerPolicy: "",
 			CrossOriginOpenerPolicy: "",
 			ExpectCTHeader:          "",
 			ExpectCTHeader:          "",
 		},
 		},
+		ExtraCSS: []httpd.CustomCSS{},
 	}
 	}
 	defaultRateLimiter = common.RateLimiterConfig{
 	defaultRateLimiter = common.RateLimiterConfig{
 		Average:                0,
 		Average:                0,
@@ -1268,6 +1269,25 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) {
 	return result, isSet
 	return result, isSet
 }
 }
 
 
+func getHTTPDExtraCSSFromEnv(idx int) []httpd.CustomCSS {
+	var css []httpd.CustomCSS
+
+	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)
+		}
+	}
+
+	return css
+}
+
 func getHTTPDWebClientIntegrationsFromEnv(idx int) []httpd.WebClientIntegration {
 func getHTTPDWebClientIntegrationsFromEnv(idx int) []httpd.WebClientIntegration {
 	var integrations []httpd.WebClientIntegration
 	var integrations []httpd.WebClientIntegration
 
 
@@ -1306,6 +1326,36 @@ func getDefaultHTTPBinding(idx int) httpd.Binding {
 	return binding
 	return binding
 }
 }
 
 
+func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
+	isSet := false
+
+	webClientIntegrations := getHTTPDWebClientIntegrationsFromEnv(idx)
+	if len(webClientIntegrations) > 0 {
+		binding.WebClientIntegrations = webClientIntegrations
+		isSet = true
+	}
+
+	oidc, ok := getHTTPDOIDCFromEnv(idx)
+	if ok {
+		binding.OIDC = oidc
+		isSet = true
+	}
+
+	securityConf, ok := getHTTPDSecurityConfFromEnv(idx)
+	if ok {
+		binding.Security = securityConf
+		isSet = true
+	}
+
+	extraCSS := getHTTPDExtraCSSFromEnv(idx)
+	if len(extraCSS) > 0 {
+		binding.ExtraCSS = extraCSS
+		isSet = true
+	}
+
+	return isSet
+}
+
 func getHTTPDBindingFromEnv(idx int) {
 func getHTTPDBindingFromEnv(idx int) {
 	binding := getDefaultHTTPBinding(idx)
 	binding := getDefaultHTTPBinding(idx)
 	isSet := false
 	isSet := false
@@ -1340,12 +1390,6 @@ func getHTTPDBindingFromEnv(idx int) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 
-	webClientIntegrations := getHTTPDWebClientIntegrationsFromEnv(idx)
-	if len(webClientIntegrations) > 0 {
-		binding.WebClientIntegrations = webClientIntegrations
-		isSet = true
-	}
-
 	enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
 	enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
 	if ok {
 	if ok {
 		binding.EnableHTTPS = enableHTTPS
 		binding.EnableHTTPS = enableHTTPS
@@ -1382,15 +1426,7 @@ func getHTTPDBindingFromEnv(idx int) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 
-	oidc, ok := getHTTPDOIDCFromEnv(idx)
-	if ok {
-		binding.OIDC = oidc
-		isSet = true
-	}
-
-	securityConf, ok := getHTTPDSecurityConfFromEnv(idx)
-	if ok {
-		binding.Security = securityConf
+	if getHTTPDNestedObjectsFromEnv(idx, &binding) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 

+ 11 - 0
config/config_test.go

@@ -828,6 +828,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__1__PORT", "8000")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__1__PORT", "8000")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS", "0")
 	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__HIDE_LOGIN_URL", " 1")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH", "")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS", "127.0.1.1")
 	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__PORT", "9000")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0")
@@ -863,6 +864,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
 	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__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")
 	t.Cleanup(func() {
 	t.Cleanup(func() {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
@@ -871,6 +874,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__PORT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__PORT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PORT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PORT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS")
@@ -906,6 +910,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER")
 		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")
 	})
 	})
 
 
 	configDir := ".."
 	configDir := ".."
@@ -929,6 +935,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "127.0.0.1", bindings[1].Address)
 	require.Equal(t, "127.0.0.1", bindings[1].Address)
 	require.False(t, bindings[1].EnableHTTPS)
 	require.False(t, bindings[1].EnableHTTPS)
 	require.Equal(t, 12, bindings[0].MinTLSVersion)
 	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].EnableWebAdmin)
 	require.True(t, bindings[1].EnableWebClient)
 	require.True(t, bindings[1].EnableWebClient)
 	require.True(t, bindings[1].RenderOpenAPI)
 	require.True(t, bindings[1].RenderOpenAPI)
@@ -936,6 +943,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 1, bindings[1].HideLoginURL)
 	require.Equal(t, 1, bindings[1].HideLoginURL)
 	require.Empty(t, bindings[1].OIDC.ClientID)
 	require.Empty(t, bindings[1].OIDC.ClientID)
 	require.False(t, bindings[1].Security.Enabled)
 	require.False(t, bindings[1].Security.Enabled)
+	require.Len(t, bindings[1].ExtraCSS, 0)
 	require.Equal(t, 9000, bindings[2].Port)
 	require.Equal(t, 9000, bindings[2].Port)
 	require.Equal(t, "127.0.1.1", bindings[2].Address)
 	require.Equal(t, "127.0.1.1", bindings[2].Address)
 	require.True(t, bindings[2].EnableHTTPS)
 	require.True(t, bindings[2].EnableHTTPS)
@@ -978,6 +986,9 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
 	require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
 	require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
 	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.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)
 }
 }
 
 
 func TestHTTPClientCertificatesFromEnv(t *testing.T) {
 func TestHTTPClientCertificatesFromEnv(t *testing.T) {

+ 3 - 3
dataprovider/dataprovider.go

@@ -2619,7 +2619,7 @@ func sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*
 }
 }
 
 
 func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
 func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
-	answers, err := client(user.Username, "", []string{"Password: "}, []bool{false})
+	answers, err := client("", "", []string{"Password: "}, []bool{false})
 	if err != nil {
 	if err != nil {
 		return 0, err
 		return 0, err
 	}
 	}
@@ -2639,7 +2639,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
 			user.Username, protocol, err)
 			user.Username, protocol, err)
 		return 0, err
 		return 0, err
 	}
 	}
-	answers, err = client(user.Username, "", []string{"Authentication code: "}, []bool{false})
+	answers, err = client("", "", []string{"Authentication code: "}, []bool{false})
 	if err != nil {
 	if err != nil {
 		return 0, err
 		return 0, err
 	}
 	}
@@ -2742,7 +2742,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
 	user *User, ip, protocol string,
 	user *User, ip, protocol string,
 ) ([]string, error) {
 ) ([]string, error) {
 	questions := response.Questions
 	questions := response.Questions
-	answers, err := client(user.Username, response.Instruction, questions, response.Echos)
+	answers, err := client("", response.Instruction, questions, response.Echos)
 	if err != nil {
 	if err != nil {
 		providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err)
 		providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err)
 		return answers, err
 		return answers, err

+ 2 - 0
docs/full-configuration.md

@@ -263,6 +263,8 @@ The configuration file contains the following sections:
       - `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank.
       - `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.
       - `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.
       - `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.
   - `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
   - `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
   - `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
   - `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

+ 1 - 1
go.mod

@@ -64,7 +64,7 @@ require (
 	golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
 	golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
 	golang.org/x/net v0.0.0-20220225172249-27dd8689420f
 	golang.org/x/net v0.0.0-20220225172249-27dd8689420f
 	golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
 	golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
-	golang.org/x/sys v0.0.0-20220318055525-2edf467146b5
+	golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8
 	golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
 	golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
 	google.golang.org/api v0.73.0
 	google.golang.org/api v0.73.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0

+ 2 - 2
go.sum

@@ -907,8 +907,8 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4=
-golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

+ 12 - 12
httpd/api_shares.go

@@ -151,9 +151,9 @@ func deleteShare(w http.ResponseWriter, r *http.Request) {
 	sendAPIResponse(w, r, err, "Share deleted", http.StatusOK)
 	sendAPIResponse(w, r, err, "Share deleted", http.StatusOK)
 }
 }
 
 
-func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -178,9 +178,9 @@ func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
 	renderAPIDirContents(w, r, contents, true)
 	renderAPIDirContents(w, r, contents, true)
 }
 }
 
 
-func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -224,9 +224,9 @@ func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 }
 }
 
 
-func downloadFromShare(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -273,12 +273,12 @@ func downloadFromShare(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 }
 }
 
 
-func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request) {
 	if maxUploadFileSize > 0 {
 	if maxUploadFileSize > 0 {
 		r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
 		r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
 	}
 	}
 	name := getURLParam(r, "name")
 	name := getURLParam(r, "name")
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -296,11 +296,11 @@ func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 }
 }
 
 
-func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
 	if maxUploadFileSize > 0 {
 	if maxUploadFileSize > 0 {
 		r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
 		r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
 	}
 	}
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
@@ -346,12 +346,12 @@ func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 }
 }
 
 
-func checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
+func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
 	isWebClient bool,
 	isWebClient bool,
 ) (dataprovider.Share, *Connection, error) {
 ) (dataprovider.Share, *Connection, error) {
 	renderError := func(err error, message string, statusCode int) {
 	renderError := func(err error, message string, statusCode int) {
 		if isWebClient {
 		if isWebClient {
-			renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
+			s.renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
 		} else {
 		} else {
 			sendAPIResponse(w, r, err, message, statusCode)
 			sendAPIResponse(w, r, err, message, statusCode)
 		}
 		}

+ 22 - 1
httpd/httpd.go

@@ -298,6 +298,14 @@ func (s *SecurityConf) getHTTPSProxyHeaders() map[string]string {
 	return headers
 	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"`
+}
+
 // WebClientIntegration defines the configuration for an external Web Client integration
 // WebClientIntegration defines the configuration for an external Web Client integration
 type WebClientIntegration struct {
 type WebClientIntegration struct {
 	// Files with these extensions can be sent to the configured URL
 	// Files with these extensions can be sent to the configured URL
@@ -354,7 +362,9 @@ type Binding struct {
 	// Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users.
 	// Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users.
 	OIDC OIDC `json:"oidc" mapstructure:"oidc"`
 	OIDC OIDC `json:"oidc" mapstructure:"oidc"`
 	// Security defines security headers to add to HTTP responses and allows to restrict allowed hosts
 	// Security defines security headers to add to HTTP responses and allows to restrict allowed hosts
-	Security         SecurityConf `json:"security" mapstructure:"security"`
+	Security SecurityConf `json:"security" mapstructure:"security"`
+	// Additional CSS
+	ExtraCSS         []CustomCSS `json:"extra_css" mapstructure:"extra_css"`
 	allowHeadersFrom []func(net.IP) bool
 	allowHeadersFrom []func(net.IP) bool
 }
 }
 
 
@@ -368,6 +378,16 @@ func (b *Binding) checkWebClientIntegrations() {
 	b.WebClientIntegrations = integrations
 	b.WebClientIntegrations = integrations
 }
 }
 
 
+func (b *Binding) checkExtraCSS() {
+	var extraCSS []CustomCSS
+	for _, css := range b.ExtraCSS {
+		extraCSS = append(extraCSS, CustomCSS{
+			Path: path.Join("/", css.Path),
+		})
+	}
+	b.ExtraCSS = extraCSS
+}
+
 func (b *Binding) parseAllowedProxy() error {
 func (b *Binding) parseAllowedProxy() error {
 	allowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)
 	allowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)
 	if err != nil {
 	if err != nil {
@@ -606,6 +626,7 @@ func (c *Conf) Initialize(configDir string) error {
 			return err
 			return err
 		}
 		}
 		binding.checkWebClientIntegrations()
 		binding.checkWebClientIntegrations()
+		binding.checkExtraCSS()
 		binding.Security.updateProxyHeaders()
 		binding.Security.updateProxyHeaders()
 
 
 		go func(b Binding) {
 		go func(b Binding) {

+ 54 - 33
httpd/internal_test.go

@@ -301,6 +301,23 @@ func TestShouldBind(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestExtraCSSValidation(t *testing.T) {
+	b := Binding{
+		ExtraCSS: []CustomCSS{
+			{
+				Path: "path1",
+			},
+			{
+				Path: "../path2",
+			},
+		},
+	}
+	b.checkExtraCSS()
+	require.Len(t, b.ExtraCSS, 2)
+	assert.Equal(t, "/path1", b.ExtraCSS[0].Path)
+	assert.Equal(t, "/path2", b.ExtraCSS[1].Path)
+}
+
 func TestRedactedConf(t *testing.T) {
 func TestRedactedConf(t *testing.T) {
 	c := Conf{
 	c := Conf{
 		SigningPassphrase: "passphrase",
 		SigningPassphrase: "passphrase",
@@ -356,6 +373,8 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
 }
 }
 
 
 func TestInvalidToken(t *testing.T) {
 func TestInvalidToken(t *testing.T) {
+	server := httpdServer{}
+	server.initializeRouter()
 	admin := dataprovider.Admin{
 	admin := dataprovider.Admin{
 		Username: "admin",
 		Username: "admin",
 	}
 	}
@@ -510,27 +529,27 @@ func TestInvalidToken(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebRestore(rr, req)
+	server.handleWebRestore(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebAddUserPost(rr, req)
+	server.handleWebAddUserPost(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebUpdateUserPost(rr, req)
+	server.handleWebUpdateUserPost(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebTemplateFolderPost(rr, req)
+	server.handleWebTemplateFolderPost(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebTemplateUserPost(rr, req)
+	server.handleWebTemplateUserPost(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
@@ -545,7 +564,7 @@ func TestInvalidToken(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebUpdateFolderPost(rr, req)
+	server.handleWebUpdateFolderPost(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
@@ -575,12 +594,10 @@ func TestInvalidToken(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebAddAdminPost(rr, req)
+	server.handleWebAddAdminPost(rr, req)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 	assert.Contains(t, rr.Body.String(), "invalid token claims")
 
 
-	server := httpdServer{}
-	server.initializeRouter()
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	server.handleWebClientTwoFactorRecoveryPost(rr, req)
 	server.handleWebClientTwoFactorRecoveryPost(rr, req)
 	assert.Equal(t, http.StatusNotFound, rr.Code)
 	assert.Equal(t, http.StatusNotFound, rr.Code)
@@ -624,7 +641,7 @@ func TestUpdateWebAdminInvalidClaims(t *testing.T) {
 	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
 	req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleWebUpdateAdminPost(rr, req)
+	server.handleWebUpdateAdminPost(rr, req)
 	assert.Equal(t, http.StatusOK, rr.Code)
 	assert.Equal(t, http.StatusOK, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 }
 }
@@ -796,13 +813,13 @@ func TestCreateTokenError(t *testing.T) {
 	req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebClientProfilePost(rr, req)
+	server.handleWebClientProfilePost(rr, req)
 	assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
 	assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
 
 
 	req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebAdminProfilePost(rr, req)
+	server.handleWebAdminProfilePost(rr, req)
 	assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
 	assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
 
 
 	req, _ = http.NewRequest(http.MethodPost, webAdminTwoFactorPath+"?a=a%C3%AO%GC", bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webAdminTwoFactorPath+"?a=a%C3%AO%GC", bytes.NewBuffer([]byte(form.Encode())))
@@ -836,14 +853,14 @@ func TestCreateTokenError(t *testing.T) {
 	req, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+"?a=a%C3%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+"?a=a%C3%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebAdminForgotPwdPost(rr, req)
+	server.handleWebAdminForgotPwdPost(rr, req)
 	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 	assert.Contains(t, rr.Body.String(), "invalid URL escape")
 	assert.Contains(t, rr.Body.String(), "invalid URL escape")
 
 
 	req, _ = http.NewRequest(http.MethodPost, webClientForgotPwdPath+"?a=a%C2%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webClientForgotPwdPath+"?a=a%C2%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
-	handleWebClientForgotPwdPost(rr, req)
+	server.handleWebClientForgotPwdPost(rr, req)
 	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 	assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 	assert.Contains(t, rr.Body.String(), "invalid URL escape")
 	assert.Contains(t, rr.Body.String(), "invalid URL escape")
 
 
@@ -939,13 +956,17 @@ func TestJWTTokenValidation(t *testing.T) {
 	token, _, err := tokenAuth.Encode(claims)
 	token, _, err := tokenAuth.Encode(claims)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
-	r := GetHTTPRouter(Binding{
-		Address:         "",
-		Port:            8080,
-		EnableWebAdmin:  true,
-		EnableWebClient: true,
-		RenderOpenAPI:   true,
-	})
+	server := httpdServer{
+		binding: Binding{
+			Address:         "",
+			Port:            8080,
+			EnableWebAdmin:  true,
+			EnableWebClient: true,
+			RenderOpenAPI:   true,
+		},
+	}
+	server.initializeRouter()
+	r := server.router
 	fn := jwtAuthenticatorAPI(r)
 	fn := jwtAuthenticatorAPI(r)
 	rr := httptest.NewRecorder()
 	rr := httptest.NewRecorder()
 	req, _ := http.NewRequest(http.MethodGet, userPath, nil)
 	req, _ := http.NewRequest(http.MethodGet, userPath, nil)
@@ -970,7 +991,7 @@ func TestJWTTokenValidation(t *testing.T) {
 	assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
 	assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
 
 
 	errTest := errors.New("test error")
 	errTest := errors.New("test error")
-	permFn := checkPerm(dataprovider.PermAdminAny)
+	permFn := server.checkPerm(dataprovider.PermAdminAny)
 	fn = permFn(r)
 	fn = permFn(r)
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodGet, userPath, nil)
 	req, _ = http.NewRequest(http.MethodGet, userPath, nil)
@@ -978,7 +999,7 @@ func TestJWTTokenValidation(t *testing.T) {
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 
 
-	permFn = checkPerm(dataprovider.PermAdminAny)
+	permFn = server.checkPerm(dataprovider.PermAdminAny)
 	fn = permFn(r)
 	fn = permFn(r)
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
 	req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
@@ -987,7 +1008,7 @@ func TestJWTTokenValidation(t *testing.T) {
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 
 
-	permClientFn := checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)
+	permClientFn := server.checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)
 	fn = permClientFn(r)
 	fn = permClientFn(r)
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
 	req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
@@ -1003,7 +1024,7 @@ func TestJWTTokenValidation(t *testing.T) {
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 
 
-	fn = checkSecondFactorRequirement(r)
+	fn = server.checkSecondFactorRequirement(r)
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
 	req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
 	req.RequestURI = webClientProfilePath
 	req.RequestURI = webClientProfilePath
@@ -1989,42 +2010,42 @@ func TestWebUserInvalidClaims(t *testing.T) {
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil)
 	req, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil)
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleWebClientDownloadZip(rr, req)
+	server.handleWebClientDownloadZip(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil)
 	req, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil)
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleClientEditFile(rr, req)
+	server.handleClientEditFile(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)
 	req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleClientUpdateShareGet(rr, req)
+	server.handleClientUpdateShareGet(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil)
 	req, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil)
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleClientAddSharePost(rr, req)
+	server.handleClientAddSharePost(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodPost, webClientSharePath+"/id", nil)
 	req, _ = http.NewRequest(http.MethodPost, webClientSharePath+"/id", nil)
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleClientUpdateSharePost(rr, req)
+	server.handleClientUpdateSharePost(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 
 
 	rr = httptest.NewRecorder()
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
 	req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleClientGetShares(rr, req)
+	server.handleClientGetShares(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 	assert.Contains(t, rr.Body.String(), "Invalid token claims")
 }
 }
@@ -2053,7 +2074,7 @@ func TestInvalidClaims(t *testing.T) {
 	req, _ := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
 	req, _ := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleWebClientProfilePost(rr, req)
+	server.handleWebClientProfilePost(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 
 
 	admin := dataprovider.Admin{
 	admin := dataprovider.Admin{
@@ -2073,7 +2094,7 @@ func TestInvalidClaims(t *testing.T) {
 	req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
 	req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
 	req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
-	handleWebAdminProfilePost(rr, req)
+	server.handleWebAdminProfilePost(rr, req)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
 }
 }
 
 

+ 17 - 17
httpd/middleware.go

@@ -87,13 +87,13 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
 	return nil
 	return nil
 }
 }
 
 
-func validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {
+func (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {
 	token, _, err := jwtauth.FromContext(r.Context())
 	token, _, err := jwtauth.FromContext(r.Context())
 	var notFoundFunc func(w http.ResponseWriter, r *http.Request, err error)
 	var notFoundFunc func(w http.ResponseWriter, r *http.Request, err error)
 	if audience == tokenAudienceWebAdminPartial {
 	if audience == tokenAudienceWebAdminPartial {
-		notFoundFunc = renderNotFoundPage
+		notFoundFunc = s.renderNotFoundPage
 	} else {
 	} else {
-		notFoundFunc = renderClientNotFoundPage
+		notFoundFunc = s.renderClientNotFoundPage
 	}
 	}
 	if err != nil || token == nil || jwt.Validate(token) != nil {
 	if err != nil || token == nil || jwt.Validate(token) != nil {
 		notFoundFunc(w, r, nil)
 		notFoundFunc(w, r, nil)
@@ -112,10 +112,10 @@ func validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience to
 	return nil
 	return nil
 }
 }
 
 
-func jwtAuthenticatorPartial(audience tokenAudience) func(next http.Handler) http.Handler {
+func (s *httpdServer) jwtAuthenticatorPartial(audience tokenAudience) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			if err := validateJWTPartialToken(w, r, audience); err != nil {
+			if err := s.validateJWTPartialToken(w, r, audience); err != nil {
 				return
 				return
 			}
 			}
 
 
@@ -169,13 +169,13 @@ func jwtAuthenticatorWebClient(next http.Handler) http.Handler {
 	})
 	})
 }
 }
 
 
-func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
+func (s *httpdServer) checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			_, claims, err := jwtauth.FromContext(r.Context())
 			_, claims, err := jwtauth.FromContext(r.Context())
 			if err != nil {
 			if err != nil {
 				if isWebRequest(r) {
 				if isWebRequest(r) {
-					renderClientBadRequestPage(w, r, err)
+					s.renderClientBadRequestPage(w, r, err)
 				} else {
 				} else {
 					sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 					sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 				}
 				}
@@ -186,7 +186,7 @@ func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
 			// for web client perms are negated and not granted
 			// for web client perms are negated and not granted
 			if tokenClaims.hasPerm(perm) {
 			if tokenClaims.hasPerm(perm) {
 				if isWebRequest(r) {
 				if isWebRequest(r) {
-					renderClientForbiddenPage(w, r, "You don't have permission for this action")
+					s.renderClientForbiddenPage(w, r, "You don't have permission for this action")
 				} else {
 				} else {
 					sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 					sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 				}
 				}
@@ -198,12 +198,12 @@ func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
 	}
 	}
 }
 }
 
 
-func checkSecondFactorRequirement(next http.Handler) http.Handler {
+func (s *httpdServer) checkSecondFactorRequirement(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		_, claims, err := jwtauth.FromContext(r.Context())
 		_, claims, err := jwtauth.FromContext(r.Context())
 		if err != nil {
 		if err != nil {
 			if isWebRequest(r) {
 			if isWebRequest(r) {
-				renderClientBadRequestPage(w, r, err)
+				s.renderClientBadRequestPage(w, r, err)
 			} else {
 			} else {
 				sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 				sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 			}
 			}
@@ -215,7 +215,7 @@ func checkSecondFactorRequirement(next http.Handler) http.Handler {
 			message := fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
 			message := fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
 				strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))
 				strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))
 			if isWebRequest(r) {
 			if isWebRequest(r) {
-				renderClientForbiddenPage(w, r, message)
+				s.renderClientForbiddenPage(w, r, message)
 			} else {
 			} else {
 				sendAPIResponse(w, r, nil, message, http.StatusForbidden)
 				sendAPIResponse(w, r, nil, message, http.StatusForbidden)
 			}
 			}
@@ -226,13 +226,13 @@ func checkSecondFactorRequirement(next http.Handler) http.Handler {
 	})
 	})
 }
 }
 
 
-func requireBuiltinLogin(next http.Handler) http.Handler {
+func (s *httpdServer) requireBuiltinLogin(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if isLoggedInWithOIDC(r) {
 		if isLoggedInWithOIDC(r) {
 			if isWebClientRequest(r) {
 			if isWebClientRequest(r) {
-				renderClientForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
+				s.renderClientForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
 			} else {
 			} else {
-				renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
+				s.renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
 			}
 			}
 			return
 			return
 		}
 		}
@@ -240,13 +240,13 @@ func requireBuiltinLogin(next http.Handler) http.Handler {
 	})
 	})
 }
 }
 
 
-func checkPerm(perm string) func(next http.Handler) http.Handler {
+func (s *httpdServer) checkPerm(perm string) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			_, claims, err := jwtauth.FromContext(r.Context())
 			_, claims, err := jwtauth.FromContext(r.Context())
 			if err != nil {
 			if err != nil {
 				if isWebRequest(r) {
 				if isWebRequest(r) {
-					renderBadRequestPage(w, r, err)
+					s.renderBadRequestPage(w, r, err)
 				} else {
 				} else {
 					sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 					sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 				}
 				}
@@ -257,7 +257,7 @@ func checkPerm(perm string) func(next http.Handler) http.Handler {
 
 
 			if !tokenClaims.hasPerm(perm) {
 			if !tokenClaims.hasPerm(perm) {
 				if isWebRequest(r) {
 				if isWebRequest(r) {
-					renderForbiddenPage(w, r, "You don't have permission for this action")
+					s.renderForbiddenPage(w, r, "You don't have permission for this action")
 				} else {
 				} else {
 					sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 					sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
 				}
 				}

+ 1 - 1
httpd/oidc.go

@@ -534,7 +534,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
 	authReq, err := oidcMgr.getPendingAuth(state)
 	authReq, err := oidcMgr.getPendingAuth(state)
 	if err != nil {
 	if err != nil {
 		logger.Debug(logSender, "", "oidc authentication state did not match")
 		logger.Debug(logSender, "", "oidc authentication state did not match")
-		renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match",
+		s.renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match",
 			http.StatusBadRequest, nil, "")
 			http.StatusBadRequest, nil, "")
 		return
 		return
 	}
 	}

+ 288 - 285
httpd/server.go

@@ -140,6 +140,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error string)
 		Error:      error,
 		Error:      error,
 		CSRFToken:  createCSRFToken(),
 		CSRFToken:  createCSRFToken(),
 		StaticURL:  webStaticFilesPath,
 		StaticURL:  webStaticFilesPath,
+		ExtraCSS:   s.binding.ExtraCSS,
 	}
 	}
 	if s.binding.showAdminLoginURL() {
 	if s.binding.showAdminLoginURL() {
 		data.AltLoginURL = webAdminLoginPath
 		data.AltLoginURL = webAdminLoginPath
@@ -166,17 +167,17 @@ func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *htt
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderClientChangePasswordPage(w, r, err.Error())
+		s.renderClientChangePasswordPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
 	err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
 		r.Form.Get("new_password2"))
 		r.Form.Get("new_password2"))
 	if err != nil {
 	if err != nil {
-		renderClientChangePasswordPage(w, r, err.Error())
+		s.renderClientChangePasswordPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	s.handleWebClientLogout(w, r)
 	s.handleWebClientLogout(w, r)
@@ -248,25 +249,25 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderClientResetPwdPage(w, err.Error())
+		s.renderClientResetPwdPage(w, err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	_, user, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), false)
 	_, user, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), false)
 	if err != nil {
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
 		if e, ok := err.(*util.ValidationError); ok {
-			renderClientResetPwdPage(w, e.GetErrorString())
+			s.renderClientResetPwdPage(w, e.GetErrorString())
 			return
 			return
 		}
 		}
-		renderClientResetPwdPage(w, err.Error())
+		s.renderClientResetPwdPage(w, err.Error())
 		return
 		return
 	}
 	}
 	connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
 	connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
 	if err := checkHTTPClientUser(user, r, connectionID); err != nil {
 	if err := checkHTTPClientUser(user, r, connectionID); err != nil {
-		renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
+		s.renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
 		return
 		return
 	}
 	}
 
 
@@ -274,210 +275,210 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
 	err = user.CheckFsRoot(connectionID)
 	err = user.CheckFsRoot(connectionID)
 	if err != nil {
 	if err != nil {
 		logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
 		logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
-		renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
+		s.renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
 		return
 		return
 	}
 	}
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
-	s.loginUser(w, r, user, connectionID, ipAddr, false, renderClientResetPwdPage)
+	s.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage)
 }
 }
 
 
 func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil {
 	if err != nil {
-		renderNotFoundPage(w, r, nil)
+		s.renderNotFoundPage(w, r, nil)
 		return
 		return
 	}
 	}
 	if err := r.ParseForm(); err != nil {
 	if err := r.ParseForm(); err != nil {
-		renderClientTwoFactorRecoveryPage(w, err.Error())
+		s.renderClientTwoFactorRecoveryPage(w, err.Error())
 		return
 		return
 	}
 	}
 	username := claims.Username
 	username := claims.Username
 	recoveryCode := r.Form.Get("recovery_code")
 	recoveryCode := r.Form.Get("recovery_code")
 	if username == "" || recoveryCode == "" {
 	if username == "" || recoveryCode == "" {
-		renderClientTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientTwoFactorRecoveryPage(w, err.Error())
+		s.renderClientTwoFactorRecoveryPage(w, err.Error())
 		return
 		return
 	}
 	}
 	user, err := dataprovider.UserExists(username)
 	user, err := dataprovider.UserExists(username)
 	if err != nil {
 	if err != nil {
-		renderClientTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
 	if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
-		renderClientTwoFactorPage(w, "Two factory authentication is not enabled")
+		s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled")
 		return
 		return
 	}
 	}
 	for idx, code := range user.Filters.RecoveryCodes {
 	for idx, code := range user.Filters.RecoveryCodes {
 		if err := code.Secret.Decrypt(); err != nil {
 		if err := code.Secret.Decrypt(); err != nil {
-			renderClientInternalServerErrorPage(w, r, fmt.Errorf("unable to decrypt recovery code: %w", err))
+			s.renderClientInternalServerErrorPage(w, r, fmt.Errorf("unable to decrypt recovery code: %w", err))
 			return
 			return
 		}
 		}
 		if code.Secret.GetPayload() == recoveryCode {
 		if code.Secret.GetPayload() == recoveryCode {
 			if code.Used {
 			if code.Used {
-				renderClientTwoFactorRecoveryPage(w, "This recovery code was already used")
+				s.renderClientTwoFactorRecoveryPage(w, "This recovery code was already used")
 				return
 				return
 			}
 			}
 			user.Filters.RecoveryCodes[idx].Used = true
 			user.Filters.RecoveryCodes[idx].Used = true
 			err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 			err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 			if err != nil {
 			if err != nil {
 				logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
 				logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
-				renderClientInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
+				s.renderClientInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
 				return
 				return
 			}
 			}
 			connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
 			connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
 			s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true,
 			s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true,
-				renderClientTwoFactorRecoveryPage)
+				s.renderClientTwoFactorRecoveryPage)
 			return
 			return
 		}
 		}
 	}
 	}
-	renderClientTwoFactorRecoveryPage(w, "Invalid recovery code")
+	s.renderClientTwoFactorRecoveryPage(w, "Invalid recovery code")
 }
 }
 
 
 func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil {
 	if err != nil {
-		renderNotFoundPage(w, r, nil)
+		s.renderNotFoundPage(w, r, nil)
 		return
 		return
 	}
 	}
 	if err := r.ParseForm(); err != nil {
 	if err := r.ParseForm(); err != nil {
-		renderClientTwoFactorPage(w, err.Error())
+		s.renderClientTwoFactorPage(w, err.Error())
 		return
 		return
 	}
 	}
 	username := claims.Username
 	username := claims.Username
 	passcode := r.Form.Get("passcode")
 	passcode := r.Form.Get("passcode")
 	if username == "" || passcode == "" {
 	if username == "" || passcode == "" {
-		renderClientTwoFactorPage(w, "Invalid credentials")
+		s.renderClientTwoFactorPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientTwoFactorPage(w, err.Error())
+		s.renderClientTwoFactorPage(w, err.Error())
 		return
 		return
 	}
 	}
 	user, err := dataprovider.UserExists(username)
 	user, err := dataprovider.UserExists(username)
 	if err != nil {
 	if err != nil {
-		renderClientTwoFactorPage(w, "Invalid credentials")
+		s.renderClientTwoFactorPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
 	if !user.Filters.TOTPConfig.Enabled || !util.IsStringInSlice(common.ProtocolHTTP, user.Filters.TOTPConfig.Protocols) {
-		renderClientTwoFactorPage(w, "Two factory authentication is not enabled")
+		s.renderClientTwoFactorPage(w, "Two factory authentication is not enabled")
 		return
 		return
 	}
 	}
 	err = user.Filters.TOTPConfig.Secret.Decrypt()
 	err = user.Filters.TOTPConfig.Secret.Decrypt()
 	if err != nil {
 	if err != nil {
-		renderClientInternalServerErrorPage(w, r, err)
+		s.renderClientInternalServerErrorPage(w, r, err)
 		return
 		return
 	}
 	}
 	match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,
 	match, err := mfa.ValidateTOTPPasscode(user.Filters.TOTPConfig.ConfigName, passcode,
 		user.Filters.TOTPConfig.Secret.GetPayload())
 		user.Filters.TOTPConfig.Secret.GetPayload())
 	if !match || err != nil {
 	if !match || err != nil {
-		renderClientTwoFactorPage(w, "Invalid authentication code")
+		s.renderClientTwoFactorPage(w, "Invalid authentication code")
 		return
 		return
 	}
 	}
 	connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
 	connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
-	s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true, renderClientTwoFactorPage)
+	s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true, s.renderClientTwoFactorPage)
 }
 }
 
 
 func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil {
 	if err != nil {
-		renderNotFoundPage(w, r, nil)
+		s.renderNotFoundPage(w, r, nil)
 		return
 		return
 	}
 	}
 	if err := r.ParseForm(); err != nil {
 	if err := r.ParseForm(); err != nil {
-		renderTwoFactorRecoveryPage(w, err.Error())
+		s.renderTwoFactorRecoveryPage(w, err.Error())
 		return
 		return
 	}
 	}
 	username := claims.Username
 	username := claims.Username
 	recoveryCode := r.Form.Get("recovery_code")
 	recoveryCode := r.Form.Get("recovery_code")
 	if username == "" || recoveryCode == "" {
 	if username == "" || recoveryCode == "" {
-		renderTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderTwoFactorRecoveryPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderTwoFactorRecoveryPage(w, err.Error())
+		s.renderTwoFactorRecoveryPage(w, err.Error())
 		return
 		return
 	}
 	}
 	admin, err := dataprovider.AdminExists(username)
 	admin, err := dataprovider.AdminExists(username)
 	if err != nil {
 	if err != nil {
-		renderTwoFactorRecoveryPage(w, "Invalid credentials")
+		s.renderTwoFactorRecoveryPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if !admin.Filters.TOTPConfig.Enabled {
 	if !admin.Filters.TOTPConfig.Enabled {
-		renderTwoFactorRecoveryPage(w, "Two factory authentication is not enabled")
+		s.renderTwoFactorRecoveryPage(w, "Two factory authentication is not enabled")
 		return
 		return
 	}
 	}
 	for idx, code := range admin.Filters.RecoveryCodes {
 	for idx, code := range admin.Filters.RecoveryCodes {
 		if err := code.Secret.Decrypt(); err != nil {
 		if err := code.Secret.Decrypt(); err != nil {
-			renderInternalServerErrorPage(w, r, fmt.Errorf("unable to decrypt recovery code: %w", err))
+			s.renderInternalServerErrorPage(w, r, fmt.Errorf("unable to decrypt recovery code: %w", err))
 			return
 			return
 		}
 		}
 		if code.Secret.GetPayload() == recoveryCode {
 		if code.Secret.GetPayload() == recoveryCode {
 			if code.Used {
 			if code.Used {
-				renderTwoFactorRecoveryPage(w, "This recovery code was already used")
+				s.renderTwoFactorRecoveryPage(w, "This recovery code was already used")
 				return
 				return
 			}
 			}
 			admin.Filters.RecoveryCodes[idx].Used = true
 			admin.Filters.RecoveryCodes[idx].Used = true
 			err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 			err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 			if err != nil {
 			if err != nil {
 				logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
 				logger.Warn(logSender, "", "unable to set the recovery code %#v as used: %v", recoveryCode, err)
-				renderInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
+				s.renderInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
 				return
 				return
 			}
 			}
-			s.loginAdmin(w, r, &admin, true, renderTwoFactorRecoveryPage)
+			s.loginAdmin(w, r, &admin, true, s.renderTwoFactorRecoveryPage)
 			return
 			return
 		}
 		}
 	}
 	}
-	renderTwoFactorRecoveryPage(w, "Invalid recovery code")
+	s.renderTwoFactorRecoveryPage(w, "Invalid recovery code")
 }
 }
 
 
 func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil {
 	if err != nil {
-		renderNotFoundPage(w, r, nil)
+		s.renderNotFoundPage(w, r, nil)
 		return
 		return
 	}
 	}
 	if err := r.ParseForm(); err != nil {
 	if err := r.ParseForm(); err != nil {
-		renderTwoFactorPage(w, err.Error())
+		s.renderTwoFactorPage(w, err.Error())
 		return
 		return
 	}
 	}
 	username := claims.Username
 	username := claims.Username
 	passcode := r.Form.Get("passcode")
 	passcode := r.Form.Get("passcode")
 	if username == "" || passcode == "" {
 	if username == "" || passcode == "" {
-		renderTwoFactorPage(w, "Invalid credentials")
+		s.renderTwoFactorPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderTwoFactorPage(w, err.Error())
+		s.renderTwoFactorPage(w, err.Error())
 		return
 		return
 	}
 	}
 	admin, err := dataprovider.AdminExists(username)
 	admin, err := dataprovider.AdminExists(username)
 	if err != nil {
 	if err != nil {
-		renderTwoFactorPage(w, "Invalid credentials")
+		s.renderTwoFactorPage(w, "Invalid credentials")
 		return
 		return
 	}
 	}
 	if !admin.Filters.TOTPConfig.Enabled {
 	if !admin.Filters.TOTPConfig.Enabled {
-		renderTwoFactorPage(w, "Two factory authentication is not enabled")
+		s.renderTwoFactorPage(w, "Two factory authentication is not enabled")
 		return
 		return
 	}
 	}
 	err = admin.Filters.TOTPConfig.Secret.Decrypt()
 	err = admin.Filters.TOTPConfig.Secret.Decrypt()
 	if err != nil {
 	if err != nil {
-		renderInternalServerErrorPage(w, r, err)
+		s.renderInternalServerErrorPage(w, r, err)
 		return
 		return
 	}
 	}
 	match, err := mfa.ValidateTOTPPasscode(admin.Filters.TOTPConfig.ConfigName, passcode,
 	match, err := mfa.ValidateTOTPPasscode(admin.Filters.TOTPConfig.ConfigName, passcode,
 		admin.Filters.TOTPConfig.Secret.GetPayload())
 		admin.Filters.TOTPConfig.Secret.GetPayload())
 	if !match || err != nil {
 	if !match || err != nil {
-		renderTwoFactorPage(w, "Invalid authentication code")
+		s.renderTwoFactorPage(w, "Invalid authentication code")
 		return
 		return
 	}
 	}
-	s.loginAdmin(w, r, &admin, true, renderTwoFactorPage)
+	s.loginAdmin(w, r, &admin, true, s.renderTwoFactorPage)
 }
 }
 
 
 func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Request) {
@@ -511,6 +512,7 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, error string)
 		Error:      error,
 		Error:      error,
 		CSRFToken:  createCSRFToken(),
 		CSRFToken:  createCSRFToken(),
 		StaticURL:  webStaticFilesPath,
 		StaticURL:  webStaticFilesPath,
+		ExtraCSS:   s.binding.ExtraCSS,
 	}
 	}
 	if s.binding.showClientLoginURL() {
 	if s.binding.showClientLoginURL() {
 		data.AltLoginURL = webClientLoginPath
 		data.AltLoginURL = webClientLoginPath
@@ -546,17 +548,17 @@ func (s *httpdServer) handleWebAdminChangePwdPost(w http.ResponseWriter, r *http
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderChangePasswordPage(w, r, err.Error())
+		s.renderChangePasswordPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderForbiddenPage(w, r, err.Error())
+		s.renderForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
 	err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
 		r.Form.Get("new_password2"))
 		r.Form.Get("new_password2"))
 	if err != nil {
 	if err != nil {
-		renderChangePasswordPage(w, r, err.Error())
+		s.renderChangePasswordPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	s.handleWebAdminLogout(w, r)
 	s.handleWebAdminLogout(w, r)
@@ -566,39 +568,39 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderResetPwdPage(w, err.Error())
+		s.renderResetPwdPage(w, err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderForbiddenPage(w, r, err.Error())
+		s.renderForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	admin, _, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), true)
 	admin, _, err := handleResetPassword(r, r.Form.Get("code"), r.Form.Get("password"), true)
 	if err != nil {
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
 		if e, ok := err.(*util.ValidationError); ok {
-			renderResetPwdPage(w, e.GetErrorString())
+			s.renderResetPwdPage(w, e.GetErrorString())
 			return
 			return
 		}
 		}
-		renderResetPwdPage(w, err.Error())
+		s.renderResetPwdPage(w, err.Error())
 		return
 		return
 	}
 	}
 
 
-	s.loginAdmin(w, r, admin, false, renderResetPwdPage)
+	s.loginAdmin(w, r, admin, false, s.renderResetPwdPage)
 }
 }
 
 
 func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Request) {
 func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	if dataprovider.HasAdmin() {
 	if dataprovider.HasAdmin() {
-		renderBadRequestPage(w, r, errors.New("an admin user already exists"))
+		s.renderBadRequestPage(w, r, errors.New("an admin user already exists"))
 		return
 		return
 	}
 	}
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderAdminSetupPage(w, r, "", err.Error())
+		s.renderAdminSetupPage(w, r, "", err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderForbiddenPage(w, r, err.Error())
+		s.renderForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	username := r.Form.Get("username")
 	username := r.Form.Get("username")
@@ -606,19 +608,19 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 	confirmPassword := r.Form.Get("confirm_password")
 	confirmPassword := r.Form.Get("confirm_password")
 	installCode := r.Form.Get("install_code")
 	installCode := r.Form.Get("install_code")
 	if installationCode != "" && installCode != installationCode {
 	if installationCode != "" && installCode != installationCode {
-		renderAdminSetupPage(w, r, username, fmt.Sprintf("%v mismatch", installationCodeHint))
+		s.renderAdminSetupPage(w, r, username, fmt.Sprintf("%v mismatch", installationCodeHint))
 		return
 		return
 	}
 	}
 	if username == "" {
 	if username == "" {
-		renderAdminSetupPage(w, r, username, "Please set a username")
+		s.renderAdminSetupPage(w, r, username, "Please set a username")
 		return
 		return
 	}
 	}
 	if password == "" {
 	if password == "" {
-		renderAdminSetupPage(w, r, username, "Please set a password")
+		s.renderAdminSetupPage(w, r, username, "Please set a password")
 		return
 		return
 	}
 	}
 	if password != confirmPassword {
 	if password != confirmPassword {
-		renderAdminSetupPage(w, r, username, "Passwords mismatch")
+		s.renderAdminSetupPage(w, r, username, "Passwords mismatch")
 		return
 		return
 	}
 	}
 	admin := dataprovider.Admin{
 	admin := dataprovider.Admin{
@@ -629,7 +631,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 	}
 	}
 	err = dataprovider.AddAdmin(&admin, username, util.GetIPFromRemoteAddress(r.RemoteAddr))
 	err = dataprovider.AddAdmin(&admin, username, util.GetIPFromRemoteAddress(r.RemoteAddr))
 	if err != nil {
 	if err != nil {
-		renderAdminSetupPage(w, r, username, err.Error())
+		s.renderAdminSetupPage(w, r, username, err.Error())
 		return
 		return
 	}
 	}
 	s.loginAdmin(w, r, &admin, false, nil)
 	s.loginAdmin(w, r, &admin, false, nil)
@@ -691,7 +693,7 @@ func (s *httpdServer) loginAdmin(
 	if err != nil {
 	if err != nil {
 		logger.Warn(logSender, "", "unable to set admin login cookie %v", err)
 		logger.Warn(logSender, "", "unable to set admin login cookie %v", err)
 		if errorFunc == nil {
 		if errorFunc == nil {
-			renderAdminSetupPage(w, r, admin.Username, err.Error())
+			s.renderAdminSetupPage(w, r, admin.Username, err.Error())
 			return
 			return
 		}
 		}
 		errorFunc(w, err.Error())
 		errorFunc(w, err.Error())
@@ -1007,11 +1009,11 @@ func (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http.
 	if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
 	if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
 		r = s.updateContextFromCookie(r)
 		r = s.updateContextFromCookie(r)
 		if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
 		if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
-			renderClientMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded",
+			s.renderClientMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded",
 				http.StatusTooManyRequests, err, "")
 				http.StatusTooManyRequests, err, "")
 			return
 			return
 		}
 		}
-		renderMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", http.StatusTooManyRequests,
+		s.renderMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", http.StatusTooManyRequests,
 			err, "")
 			err, "")
 		return
 		return
 	}
 	}
@@ -1022,10 +1024,10 @@ func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Reque
 	if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
 	if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
 		r = s.updateContextFromCookie(r)
 		r = s.updateContextFromCookie(r)
 		if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
 		if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
-			renderClientForbiddenPage(w, r, message)
+			s.renderClientForbiddenPage(w, r, message)
 			return
 			return
 		}
 		}
-		renderForbiddenPage(w, r, message)
+		s.renderForbiddenPage(w, r, message)
 		return
 		return
 	}
 	}
 	sendAPIResponse(w, r, errors.New(message), message, http.StatusForbidden)
 	sendAPIResponse(w, r, errors.New(message), message, http.StatusForbidden)
@@ -1109,10 +1111,10 @@ func (s *httpdServer) initializeRouter() {
 		if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
 		if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
 			r = s.updateContextFromCookie(r)
 			r = s.updateContextFromCookie(r)
 			if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
 			if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
-				renderClientNotFoundPage(w, r, nil)
+				s.renderClientNotFoundPage(w, r, nil)
 				return
 				return
 			}
 			}
-			renderNotFoundPage(w, r, nil)
+			s.renderNotFoundPage(w, r, nil)
 			return
 			return
 		}
 		}
 		sendAPIResponse(w, r, nil, http.StatusText(http.StatusNotFound), http.StatusNotFound)
 		sendAPIResponse(w, r, nil, http.StatusText(http.StatusNotFound), http.StatusNotFound)
@@ -1123,11 +1125,11 @@ func (s *httpdServer) initializeRouter() {
 	})
 	})
 
 
 	// share API exposed to external users
 	// share API exposed to external users
-	s.router.Get(sharesPath+"/{id}", downloadFromShare)
-	s.router.Post(sharesPath+"/{id}", uploadFilesToShare)
-	s.router.Post(sharesPath+"/{id}/{name}", uploadFileToShare)
-	s.router.With(compressor.Handler).Get(sharesPath+"/{id}/dirs", readBrowsableShareContents)
-	s.router.Get(sharesPath+"/{id}/files", downloadBrowsableSharedFile)
+	s.router.Get(sharesPath+"/{id}", s.downloadFromShare)
+	s.router.Post(sharesPath+"/{id}", s.uploadFilesToShare)
+	s.router.Post(sharesPath+"/{id}/{name}", s.uploadFileToShare)
+	s.router.With(compressor.Handler).Get(sharesPath+"/{id}/dirs", s.readBrowsableShareContents)
+	s.router.Get(sharesPath+"/{id}/files", s.downloadBrowsableSharedFile)
 
 
 	s.router.Get(tokenPath, s.getToken)
 	s.router.Get(tokenPath, s.getToken)
 	s.router.Post(adminPath+"/{username}/forgot-password", forgotAdminPassword)
 	s.router.Post(adminPath+"/{username}/forgot-password", forgotAdminPassword)
@@ -1159,81 +1161,81 @@ func (s *httpdServer) initializeRouter() {
 		router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)
 		router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)
 		router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)
 		router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)
 
 
-		router.With(checkPerm(dataprovider.PermAdminViewServerStatus)).
+		router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)).
 			Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
 			Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
 				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 				render.JSON(w, r, getServicesStatus())
 				render.JSON(w, r, getServicesStatus())
 			})
 			})
 
 
-		router.With(checkPerm(dataprovider.PermAdminViewConnections)).
+		router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).
 			Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
 			Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
 				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 				render.JSON(w, r, common.Connections.GetStats())
 				render.JSON(w, r, common.Connections.GetStats())
 			})
 			})
 
 
-		router.With(checkPerm(dataprovider.PermAdminCloseConnections)).
+		router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)).
 			Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
 			Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanPath, getUsersQuotaScans)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanPath, startUserQuotaScanCompat)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanVFolderPath, getFoldersQuotaScans)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanVFolderPath, startFolderQuotaScanCompat)
-		router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
-		router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
-		router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
-		router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
-		router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
-		router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders)
-		router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath+"/{name}", getFolderByName)
-		router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(folderPath+"/{name}", updateFolder)
-		router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath+"/{name}", deleteFolder)
-		router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
-		router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
-		router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsageCompat)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage",
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanPath, getUsersQuotaScans)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanPath, startUserQuotaScanCompat)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanVFolderPath, getFoldersQuotaScans)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanVFolderPath, startFolderQuotaScanCompat)
+		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
+		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
+		router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
+		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername)
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
+		router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
+		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders)
+		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath+"/{name}", getFolderByName)
+		router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(folderPath+"/{name}", updateFolder)
+		router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath+"/{name}", deleteFolder)
+		router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
+		router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
+		router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsageCompat)
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage",
 			updateUserQuotaUsage)
 			updateUserQuotaUsage)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage",
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage",
 			updateUserTransferQuotaUsage)
 			updateUserTransferQuotaUsage)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateFolderQuotaUsageCompat)
-		router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage",
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateFolderQuotaUsageCompat)
+		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage",
 			updateFolderQuotaUsage)
 			updateFolderQuotaUsage)
-		router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
-		router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
-		router.With(checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
-		router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderBanTime, getBanTime)
-		router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderScore, getScore)
-		router.With(checkPerm(dataprovider.PermAdminManageDefender)).Post(defenderUnban, unban)
-		router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
-		router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
-		router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
-		router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
-		router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
-		router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
-		router.With(checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
-		router.With(checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
+		router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
+		router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
+		router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
+		router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderBanTime, getBanTime)
+		router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderScore, getScore)
+		router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Post(defenderUnban, unban)
+		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
+		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
+		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
+		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
+		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
+		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
+		router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
+		router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
 			startRetentionCheck)
 			startRetentionCheck)
-		router.With(checkPerm(dataprovider.PermAdminMetadataChecks)).Get(metadataChecksPath, getMetadataChecks)
-		router.With(checkPerm(dataprovider.PermAdminMetadataChecks)).Post(metadataBasePath+"/{username}/check",
+		router.With(s.checkPerm(dataprovider.PermAdminMetadataChecks)).Get(metadataChecksPath, getMetadataChecks)
+		router.With(s.checkPerm(dataprovider.PermAdminMetadataChecks)).Post(metadataBasePath+"/{username}/check",
 			startMetadataCheck)
 			startMetadataCheck)
-		router.With(checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+		router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
 			Get(fsEventsPath, searchFsEvents)
 			Get(fsEventsPath, searchFsEvents)
-		router.With(checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+		router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
 			Get(providerEventsPath, searchProviderEvents)
 			Get(providerEventsPath, searchProviderEvents)
-		router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
+		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 			Get(apiKeysPath, getAPIKeys)
 			Get(apiKeysPath, getAPIKeys)
-		router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
+		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 			Post(apiKeysPath, addAPIKey)
 			Post(apiKeysPath, addAPIKey)
-		router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
+		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 			Get(apiKeysPath+"/{id}", getAPIKeyByID)
 			Get(apiKeysPath+"/{id}", getAPIKeyByID)
-		router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
+		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 			Put(apiKeysPath+"/{id}", updateAPIKey)
 			Put(apiKeysPath+"/{id}", updateAPIKey)
-		router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
+		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 			Delete(apiKeysPath+"/{id}", deleteAPIKey)
 			Delete(apiKeysPath+"/{id}", deleteAPIKey)
 	})
 	})
 
 
@@ -1245,60 +1247,60 @@ func (s *httpdServer) initializeRouter() {
 		router.Use(jwtAuthenticatorAPIUser)
 		router.Use(jwtAuthenticatorAPIUser)
 
 
 		router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)
 		router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)
-		router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement,
-			checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).Put(userPwdPath, changeUserPassword)
-		router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement,
-			checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys)
-		router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement,
-			checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys)
+		router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement,
+			s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).Put(userPwdPath, changeUserPassword)
+		router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement,
+			s.checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys)
+		router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement,
+			s.checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys)
 		router.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)
 		router.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)
-		router.With(forbidAPIKeyAuthentication, checkSecondFactorRequirement).Put(userProfilePath, updateUserProfile)
+		router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement).Put(userProfilePath, updateUserProfile)
 		// user TOTP APIs
 		// user TOTP APIs
-		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Get(userTOTPConfigsPath, getTOTPConfigs)
 			Get(userTOTPConfigsPath, getTOTPConfigs)
-		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Post(userTOTPGeneratePath, generateTOTPSecret)
 			Post(userTOTPGeneratePath, generateTOTPSecret)
-		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Post(userTOTPValidatePath, validateTOTPPasscode)
 			Post(userTOTPValidatePath, validateTOTPPasscode)
-		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Post(userTOTPSavePath, saveTOTPConfig)
 			Post(userTOTPSavePath, saveTOTPConfig)
-		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Get(user2FARecoveryCodesPath, getRecoveryCodes)
 			Get(user2FARecoveryCodesPath, getRecoveryCodes)
-		router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
 			Post(user2FARecoveryCodesPath, generateRecoveryCodes)
 			Post(user2FARecoveryCodesPath, generateRecoveryCodes)
 
 
 		// compatibility layer to remove in v2.3
 		// compatibility layer to remove in v2.3
-		router.With(checkSecondFactorRequirement, compressor.Handler).Get(userFolderPath, readUserFolder)
-		router.With(checkSecondFactorRequirement).Get(userFilePath, getUserFile)
+		router.With(s.checkSecondFactorRequirement, compressor.Handler).Get(userFolderPath, readUserFolder)
+		router.With(s.checkSecondFactorRequirement).Get(userFilePath, getUserFile)
 
 
-		router.With(checkSecondFactorRequirement, compressor.Handler).Get(userDirsPath, readUserFolder)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, compressor.Handler).Get(userDirsPath, readUserFolder)
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Post(userDirsPath, createUserDir)
 			Post(userDirsPath, createUserDir)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Patch(userDirsPath, renameUserDir)
 			Patch(userDirsPath, renameUserDir)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Delete(userDirsPath, deleteUserDir)
 			Delete(userDirsPath, deleteUserDir)
-		router.With(checkSecondFactorRequirement).Get(userFilesPath, getUserFile)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement).Get(userFilesPath, getUserFile)
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Post(userFilesPath, uploadUserFiles)
 			Post(userFilesPath, uploadUserFiles)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Patch(userFilesPath, renameUserFile)
 			Patch(userFilesPath, renameUserFile)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Delete(userFilesPath, deleteUserFile)
 			Delete(userFilesPath, deleteUserFile)
-		router.With(checkSecondFactorRequirement).Post(userStreamZipPath, getUserFilesAsZipStream)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+		router.With(s.checkSecondFactorRequirement).Post(userStreamZipPath, getUserFilesAsZipStream)
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
 			Get(userSharesPath, getShares)
 			Get(userSharesPath, getShares)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
 			Post(userSharesPath, addShare)
 			Post(userSharesPath, addShare)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
 			Get(userSharesPath+"/{id}", getShareByID)
 			Get(userSharesPath+"/{id}", getShareByID)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
 			Put(userSharesPath+"/{id}", updateShare)
 			Put(userSharesPath+"/{id}", updateShare)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
 			Delete(userSharesPath+"/{id}", deleteShare)
 			Delete(userSharesPath+"/{id}", deleteShare)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Post(userUploadFilePath, uploadUserFile)
 			Post(userUploadFilePath, uploadUserFile)
-		router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
 			Patch(userFilesDirsMetadataPath, setFileDirMetadata)
 			Patch(userFilesDirsMetadataPath, setFileDirMetadata)
 	})
 	})
 
 
@@ -1353,29 +1355,29 @@ func (s *httpdServer) setupWebClientRoutes() {
 			s.router.Get(webClientOIDCLoginPath, s.handleWebClientOIDCLogin)
 			s.router.Get(webClientOIDCLoginPath, s.handleWebClientOIDCLogin)
 		}
 		}
 		s.router.Post(webClientLoginPath, s.handleWebClientLoginPost)
 		s.router.Post(webClientLoginPath, s.handleWebClientLoginPost)
-		s.router.Get(webClientForgotPwdPath, handleWebClientForgotPwd)
-		s.router.Post(webClientForgotPwdPath, handleWebClientForgotPwdPost)
-		s.router.Get(webClientResetPwdPath, handleWebClientPasswordReset)
+		s.router.Get(webClientForgotPwdPath, s.handleWebClientForgotPwd)
+		s.router.Post(webClientForgotPwdPath, s.handleWebClientForgotPwdPost)
+		s.router.Get(webClientResetPwdPath, s.handleWebClientPasswordReset)
 		s.router.Post(webClientResetPwdPath, s.handleWebClientPasswordResetPost)
 		s.router.Post(webClientResetPwdPath, s.handleWebClientPasswordResetPost)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
-			Get(webClientTwoFactorPath, handleWebClientTwoFactor)
+			s.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
+			Get(webClientTwoFactorPath, s.handleWebClientTwoFactor)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
+			s.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
 			Post(webClientTwoFactorPath, s.handleWebClientTwoFactorPost)
 			Post(webClientTwoFactorPath, s.handleWebClientTwoFactorPost)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
-			Get(webClientTwoFactorRecoveryPath, handleWebClientTwoFactorRecovery)
+			s.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
+			Get(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecovery)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
+			s.jwtAuthenticatorPartial(tokenAudienceWebClientPartial)).
 			Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
 			Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
 		// share API exposed to external users
 		// share API exposed to external users
-		s.router.Get(webClientPubSharesPath+"/{id}", downloadFromShare)
-		s.router.Get(webClientPubSharesPath+"/{id}/browse", handleShareGetFiles)
-		s.router.Get(webClientPubSharesPath+"/{id}/upload", handleClientUploadToShare)
-		s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", handleShareGetDirContents)
-		s.router.Post(webClientPubSharesPath+"/{id}", uploadFilesToShare)
-		s.router.Post(webClientPubSharesPath+"/{id}/{name}", uploadFileToShare)
+		s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
+		s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
+		s.router.Get(webClientPubSharesPath+"/{id}/upload", s.handleClientUploadToShare)
+		s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", s.handleShareGetDirContents)
+		s.router.Post(webClientPubSharesPath+"/{id}", s.uploadFilesToShare)
+		s.router.Post(webClientPubSharesPath+"/{id}/{name}", s.uploadFileToShare)
 
 
 		s.router.Group(func(router chi.Router) {
 		s.router.Group(func(router chi.Router) {
 			if s.binding.OIDC.isEnabled() {
 			if s.binding.OIDC.isEnabled() {
@@ -1385,57 +1387,57 @@ func (s *httpdServer) setupWebClientRoutes() {
 			router.Use(jwtAuthenticatorWebClient)
 			router.Use(jwtAuthenticatorWebClient)
 
 
 			router.Get(webClientLogoutPath, s.handleWebClientLogout)
 			router.Get(webClientLogoutPath, s.handleWebClientLogout)
-			router.With(checkSecondFactorRequirement, s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
-			router.With(checkSecondFactorRequirement, s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
-			router.With(checkSecondFactorRequirement, s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
+			router.With(s.checkSecondFactorRequirement, s.refreshCookie).Get(webClientViewPDFPath, s.handleClientViewPDF)
+			router.With(s.checkSecondFactorRequirement, s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
 				Post(webClientFilePath, uploadUserFile)
 				Post(webClientFilePath, uploadUserFile)
-			router.With(checkSecondFactorRequirement, s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.refreshCookie).Get(webClientEditFilePath, s.handleClientEditFile)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
 				Patch(webClientFilesPath, renameUserFile)
 				Patch(webClientFilesPath, renameUserFile)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
 				Delete(webClientFilesPath, deleteUserFile)
 				Delete(webClientFilesPath, deleteUserFile)
-			router.With(checkSecondFactorRequirement, compressor.Handler, s.refreshCookie).
+			router.With(s.checkSecondFactorRequirement, compressor.Handler, s.refreshCookie).
 				Get(webClientDirsPath, s.handleClientGetDirContents)
 				Get(webClientDirsPath, s.handleClientGetDirContents)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
 				Post(webClientDirsPath, createUserDir)
 				Post(webClientDirsPath, createUserDir)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
 				Patch(webClientDirsPath, renameUserDir)
 				Patch(webClientDirsPath, renameUserDir)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
 				Delete(webClientDirsPath, deleteUserDir)
 				Delete(webClientDirsPath, deleteUserDir)
-			router.With(checkSecondFactorRequirement, s.refreshCookie).
-				Get(webClientDownloadZipPath, handleWebClientDownloadZip)
-			router.With(checkSecondFactorRequirement, s.refreshCookie, requireBuiltinLogin).
-				Get(webClientProfilePath, handleClientGetProfile)
-			router.With(checkSecondFactorRequirement, requireBuiltinLogin).
-				Post(webClientProfilePath, handleWebClientProfilePost)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
-				Get(webChangeClientPwdPath, handleWebClientChangePwd)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
+			router.With(s.checkSecondFactorRequirement, s.refreshCookie).
+				Get(webClientDownloadZipPath, s.handleWebClientDownloadZip)
+			router.With(s.checkSecondFactorRequirement, s.refreshCookie, s.requireBuiltinLogin).
+				Get(webClientProfilePath, s.handleClientGetProfile)
+			router.With(s.checkSecondFactorRequirement, s.requireBuiltinLogin).
+				Post(webClientProfilePath, s.handleWebClientProfilePost)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
+				Get(webChangeClientPwdPath, s.handleWebClientChangePwd)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
 				Post(webChangeClientPwdPath, s.handleWebClientChangePwdPost)
 				Post(webChangeClientPwdPath, s.handleWebClientChangePwdPost)
-			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
-				Get(webClientMFAPath, handleWebClientMFA)
-			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
+			router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
+				Get(webClientMFAPath, s.handleWebClientMFA)
+			router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
 				Post(webClientTOTPGeneratePath, generateTOTPSecret)
 				Post(webClientTOTPGeneratePath, generateTOTPSecret)
-			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
+			router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
 				Post(webClientTOTPValidatePath, validateTOTPPasscode)
 				Post(webClientTOTPValidatePath, validateTOTPPasscode)
-			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
+			router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
 				Post(webClientTOTPSavePath, saveTOTPConfig)
 				Post(webClientTOTPSavePath, saveTOTPConfig)
-			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader, s.refreshCookie).
+			router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader, s.refreshCookie).
 				Get(webClientRecoveryCodesPath, getRecoveryCodes)
 				Get(webClientRecoveryCodesPath, getRecoveryCodes)
-			router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
+			router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
 				Post(webClientRecoveryCodesPath, generateRecoveryCodes)
 				Post(webClientRecoveryCodesPath, generateRecoveryCodes)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
-				Get(webClientSharesPath, handleClientGetShares)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
-				Get(webClientSharePath, handleClientAddShareGet)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-				Post(webClientSharePath, handleClientAddSharePost)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
-				Get(webClientSharePath+"/{id}", handleClientUpdateShareGet)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-				Post(webClientSharePath+"/{id}", handleClientUpdateSharePost)
-			router.With(checkSecondFactorRequirement, checkHTTPUserPerm(sdk.WebClientSharesDisabled), verifyCSRFHeader).
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
+				Get(webClientSharesPath, s.handleClientGetShares)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
+				Get(webClientSharePath, s.handleClientAddShareGet)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Post(webClientSharePath, s.handleClientAddSharePost)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
+				Get(webClientSharePath+"/{id}", s.handleClientUpdateShareGet)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Post(webClientSharePath+"/{id}", s.handleClientUpdateSharePost)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), verifyCSRFHeader).
 				Delete(webClientSharePath+"/{id}", deleteShare)
 				Delete(webClientSharePath+"/{id}", deleteShare)
 		})
 		})
 	}
 	}
@@ -1452,23 +1454,23 @@ func (s *httpdServer) setupWebAdminRoutes() {
 			s.router.Get(webAdminOIDCLoginPath, s.handleWebAdminOIDCLogin)
 			s.router.Get(webAdminOIDCLoginPath, s.handleWebAdminOIDCLogin)
 		}
 		}
 		s.router.Post(webAdminLoginPath, s.handleWebAdminLoginPost)
 		s.router.Post(webAdminLoginPath, s.handleWebAdminLoginPost)
-		s.router.Get(webAdminSetupPath, handleWebAdminSetupGet)
+		s.router.Get(webAdminSetupPath, s.handleWebAdminSetupGet)
 		s.router.Post(webAdminSetupPath, s.handleWebAdminSetupPost)
 		s.router.Post(webAdminSetupPath, s.handleWebAdminSetupPost)
-		s.router.Get(webAdminForgotPwdPath, handleWebAdminForgotPwd)
-		s.router.Post(webAdminForgotPwdPath, handleWebAdminForgotPwdPost)
-		s.router.Get(webAdminResetPwdPath, handleWebAdminPasswordReset)
+		s.router.Get(webAdminForgotPwdPath, s.handleWebAdminForgotPwd)
+		s.router.Post(webAdminForgotPwdPath, s.handleWebAdminForgotPwdPost)
+		s.router.Get(webAdminResetPwdPath, s.handleWebAdminPasswordReset)
 		s.router.Post(webAdminResetPwdPath, s.handleWebAdminPasswordResetPost)
 		s.router.Post(webAdminResetPwdPath, s.handleWebAdminPasswordResetPost)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
-			Get(webAdminTwoFactorPath, handleWebAdminTwoFactor)
+			s.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
+			Get(webAdminTwoFactorPath, s.handleWebAdminTwoFactor)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
+			s.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
 			Post(webAdminTwoFactorPath, s.handleWebAdminTwoFactorPost)
 			Post(webAdminTwoFactorPath, s.handleWebAdminTwoFactorPost)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
-			Get(webAdminTwoFactorRecoveryPath, handleWebAdminTwoFactorRecovery)
+			s.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
+			Get(webAdminTwoFactorRecoveryPath, s.handleWebAdminTwoFactorRecovery)
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
 		s.router.With(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie),
-			jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
+			s.jwtAuthenticatorPartial(tokenAudienceWebAdminPartial)).
 			Post(webAdminTwoFactorRecoveryPath, s.handleWebAdminTwoFactorRecoveryPost)
 			Post(webAdminTwoFactorRecoveryPath, s.handleWebAdminTwoFactorRecoveryPost)
 
 
 		s.router.Group(func(router chi.Router) {
 		s.router.Group(func(router chi.Router) {
@@ -1479,72 +1481,73 @@ func (s *httpdServer) setupWebAdminRoutes() {
 			router.Use(jwtAuthenticatorWebAdmin)
 			router.Use(jwtAuthenticatorWebAdmin)
 
 
 			router.Get(webLogoutPath, s.handleWebAdminLogout)
 			router.Get(webLogoutPath, s.handleWebAdminLogout)
-			router.With(s.refreshCookie, requireBuiltinLogin).Get(webAdminProfilePath, handleWebAdminProfile)
-			router.With(requireBuiltinLogin).Post(webAdminProfilePath, handleWebAdminProfilePost)
-			router.With(s.refreshCookie, requireBuiltinLogin).Get(webChangeAdminPwdPath, handleWebAdminChangePwd)
-			router.With(requireBuiltinLogin).Post(webChangeAdminPwdPath, s.handleWebAdminChangePwdPost)
-
-			router.With(s.refreshCookie, requireBuiltinLogin).Get(webAdminMFAPath, handleWebAdminMFA)
-			router.With(verifyCSRFHeader, requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
-			router.With(verifyCSRFHeader, requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
-			router.With(verifyCSRFHeader, requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)
-			router.With(verifyCSRFHeader, requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath, getRecoveryCodes)
-			router.With(verifyCSRFHeader, requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
-
-			router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
-				Get(webUsersPath, handleGetWebUsers)
-			router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
-				Get(webUserPath, handleWebAddUserGet)
-			router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
-				Get(webUserPath+"/{username}", handleWebUpdateUserGet)
-			router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, handleWebAddUserPost)
-			router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}", handleWebUpdateUserPost)
-			router.With(checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
-				Get(webConnectionsPath, handleWebGetConnections)
-			router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
-				Get(webFoldersPath, handleWebGetFolders)
-			router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
-				Get(webFolderPath, handleWebAddFolderGet)
-			router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(webFolderPath, handleWebAddFolderPost)
-			router.With(checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie).
-				Get(webStatusPath, handleWebGetStatus)
-			router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
-				Get(webAdminsPath, handleGetWebAdmins)
-			router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
-				Get(webAdminPath, handleWebAddAdminGet)
-			router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
-				Get(webAdminPath+"/{username}", handleWebUpdateAdminGet)
-			router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, handleWebAddAdminPost)
-			router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}",
-				handleWebUpdateAdminPost)
-			router.With(checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
+			router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminProfilePath, s.handleWebAdminProfile)
+			router.With(s.requireBuiltinLogin).Post(webAdminProfilePath, s.handleWebAdminProfilePost)
+			router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webChangeAdminPwdPath, s.handleWebAdminChangePwd)
+			router.With(s.requireBuiltinLogin).Post(webChangeAdminPwdPath, s.handleWebAdminChangePwdPost)
+
+			router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminMFAPath, s.handleWebAdminMFA)
+			router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
+			router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
+			router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)
+			router.With(verifyCSRFHeader, s.requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath, getRecoveryCodes)
+			router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
+
+			router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
+				Get(webUsersPath, s.handleGetWebUsers)
+			router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
+				Get(webUserPath, s.handleWebAddUserGet)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
+				Get(webUserPath+"/{username}", s.handleWebUpdateUserGet)
+			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}",
+				s.handleWebUpdateUserPost)
+			router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
+				Get(webConnectionsPath, s.handleWebGetConnections)
+			router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
+				Get(webFoldersPath, s.handleWebGetFolders)
+			router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
+				Get(webFolderPath, s.handleWebAddFolderGet)
+			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webFolderPath, s.handleWebAddFolderPost)
+			router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie).
+				Get(webStatusPath, s.handleWebGetStatus)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
+				Get(webAdminsPath, s.handleGetWebAdmins)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
+				Get(webAdminPath, s.handleWebAddAdminGet)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
+				Get(webAdminPath+"/{username}", s.handleWebUpdateAdminGet)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, s.handleWebAddAdminPost)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}",
+				s.handleWebUpdateAdminPost)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
 				Delete(webAdminPath+"/{username}", deleteAdmin)
 				Delete(webAdminPath+"/{username}", deleteAdmin)
-			router.With(checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
+			router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
 				Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
 				Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
-			router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
-				Get(webFolderPath+"/{name}", handleWebUpdateFolderGet)
-			router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webFolderPath+"/{name}",
-				handleWebUpdateFolderPost)
-			router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
+				Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webFolderPath+"/{name}",
+				s.handleWebUpdateFolderPost)
+			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
 				Delete(webFolderPath+"/{name}", deleteFolder)
 				Delete(webFolderPath+"/{name}", deleteFolder)
-			router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
+			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
 				Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
 				Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
-			router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
+			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
 				Delete(webUserPath+"/{username}", deleteUser)
 				Delete(webUserPath+"/{username}", deleteUser)
-			router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
+			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
 				Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
 				Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, handleWebMaintenance)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, handleWebRestore)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
-				Get(webTemplateUser, handleWebTemplateUserGet)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, handleWebTemplateUserPost)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
-				Get(webTemplateFolder, handleWebTemplateFolderGet)
-			router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, handleWebTemplateFolderPost)
-			router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, handleWebDefenderPage)
-			router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
-			router.With(checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, s.handleWebRestore)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
+				Get(webTemplateUser, s.handleWebTemplateUserGet)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, s.handleWebTemplateUserPost)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
+				Get(webTemplateFolder, s.handleWebTemplateFolderGet)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, s.handleWebTemplateFolderPost)
+			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, s.handleWebDefenderPage)
+			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
+			router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
 				deleteDefenderHostByID)
 				deleteDefenderHostByID)
 		})
 		})
 	}
 	}

+ 4 - 0
httpd/web.go

@@ -32,6 +32,7 @@ type loginPage struct {
 	AltLoginURL    string
 	AltLoginURL    string
 	ForgotPwdURL   string
 	ForgotPwdURL   string
 	OpenIDLoginURL string
 	OpenIDLoginURL string
+	ExtraCSS       []CustomCSS
 }
 }
 
 
 type twoFactorPage struct {
 type twoFactorPage struct {
@@ -41,6 +42,7 @@ type twoFactorPage struct {
 	CSRFToken   string
 	CSRFToken   string
 	StaticURL   string
 	StaticURL   string
 	RecoveryURL string
 	RecoveryURL string
+	ExtraCSS    []CustomCSS
 }
 }
 
 
 type forgotPwdPage struct {
 type forgotPwdPage struct {
@@ -49,6 +51,7 @@ type forgotPwdPage struct {
 	CSRFToken  string
 	CSRFToken  string
 	StaticURL  string
 	StaticURL  string
 	Title      string
 	Title      string
+	ExtraCSS   []CustomCSS
 }
 }
 
 
 type resetPwdPage struct {
 type resetPwdPage struct {
@@ -57,6 +60,7 @@ type resetPwdPage struct {
 	CSRFToken  string
 	CSRFToken  string
 	StaticURL  string
 	StaticURL  string
 	Title      string
 	Title      string
+	ExtraCSS   []CustomCSS
 }
 }
 
 
 func getSliceFromDelimitedValues(values, delimiter string) []string {
 func getSliceFromDelimitedValues(values, delimiter string) []string {

File diff suppressed because it is too large
+ 183 - 172
httpd/webadmin.go


+ 139 - 129
httpd/webclient.go

@@ -98,6 +98,7 @@ type baseClientPage struct {
 	CSRFToken        string
 	CSRFToken        string
 	HasExternalLogin bool
 	HasExternalLogin bool
 	LoggedUser       *dataprovider.User
 	LoggedUser       *dataprovider.User
+	ExtraCSS         []CustomCSS
 }
 }
 
 
 type dirMapping struct {
 type dirMapping struct {
@@ -109,6 +110,7 @@ type viewPDFPage struct {
 	Title     string
 	Title     string
 	URL       string
 	URL       string
 	StaticURL string
 	StaticURL string
+	ExtraCSS  []CustomCSS
 }
 }
 
 
 type editFilePage struct {
 type editFilePage struct {
@@ -309,7 +311,7 @@ func loadClientTemplates(templatesPath string) {
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
 	clientTemplates[templateUploadToShare] = shareUploadTmpl
 }
 }
 
 
-func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
+func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
 	var csrfToken string
 	var csrfToken string
 	if currentURL != "" {
 	if currentURL != "" {
 		csrfToken = createCSRFToken()
 		csrfToken = createCSRFToken()
@@ -335,27 +337,30 @@ func getBaseClientPageData(title, currentURL string, r *http.Request) baseClient
 		CSRFToken:        csrfToken,
 		CSRFToken:        csrfToken,
 		HasExternalLogin: isLoggedInWithOIDC(r),
 		HasExternalLogin: isLoggedInWithOIDC(r),
 		LoggedUser:       getUserFromToken(r),
 		LoggedUser:       getUserFromToken(r),
+		ExtraCSS:         s.binding.ExtraCSS,
 	}
 	}
 }
 }
 
 
-func renderClientForgotPwdPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error string) {
 	data := forgotPwdPage{
 	data := forgotPwdPage{
 		CurrentURL: webClientForgotPwdPath,
 		CurrentURL: webClientForgotPwdPath,
 		Error:      error,
 		Error:      error,
 		CSRFToken:  createCSRFToken(),
 		CSRFToken:  createCSRFToken(),
 		StaticURL:  webStaticFilesPath,
 		StaticURL:  webStaticFilesPath,
 		Title:      pageClientForgotPwdTitle,
 		Title:      pageClientForgotPwdTitle,
+		ExtraCSS:   s.binding.ExtraCSS,
 	}
 	}
 	renderClientTemplate(w, templateForgotPassword, data)
 	renderClientTemplate(w, templateForgotPassword, data)
 }
 }
 
 
-func renderClientResetPwdPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error string) {
 	data := resetPwdPage{
 	data := resetPwdPage{
 		CurrentURL: webClientResetPwdPath,
 		CurrentURL: webClientResetPwdPath,
 		Error:      error,
 		Error:      error,
 		CSRFToken:  createCSRFToken(),
 		CSRFToken:  createCSRFToken(),
 		StaticURL:  webStaticFilesPath,
 		StaticURL:  webStaticFilesPath,
 		Title:      pageClientResetPwdTitle,
 		Title:      pageClientResetPwdTitle,
+		ExtraCSS:   s.binding.ExtraCSS,
 	}
 	}
 	renderClientTemplate(w, templateResetPassword, data)
 	renderClientTemplate(w, templateResetPassword, data)
 }
 }
@@ -367,7 +372,7 @@ func renderClientTemplate(w http.ResponseWriter, tmplName string, data interface
 	}
 	}
 }
 }
 
 
-func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
+func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
 	var errorString string
 	var errorString string
 	if body != "" {
 	if body != "" {
 		errorString = body + " "
 		errorString = body + " "
@@ -376,7 +381,7 @@ func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body
 		errorString += err.Error()
 		errorString += err.Error()
 	}
 	}
 	data := clientMessagePage{
 	data := clientMessagePage{
-		baseClientPage: getBaseClientPageData(title, "", r),
+		baseClientPage: s.getBaseClientPageData(title, "", r),
 		Error:          errorString,
 		Error:          errorString,
 		Success:        message,
 		Success:        message,
 	}
 	}
@@ -384,23 +389,23 @@ func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body
 	renderClientTemplate(w, templateClientMessage, data)
 	renderClientTemplate(w, templateClientMessage, data)
 }
 }
 
 
-func renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
-	renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
+func (s *httpdServer) renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
+	s.renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
 }
 }
 
 
-func renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
-	renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
+func (s *httpdServer) renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
+	s.renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
 }
 }
 
 
-func renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
-	renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
+func (s *httpdServer) renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
+	s.renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
 }
 }
 
 
-func renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
-	renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
+func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
+	s.renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
 }
 }
 
 
-func renderClientTwoFactorPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error string) {
 	data := twoFactorPage{
 	data := twoFactorPage{
 		CurrentURL:  webClientTwoFactorPath,
 		CurrentURL:  webClientTwoFactorPath,
 		Version:     version.Get().Version,
 		Version:     version.Get().Version,
@@ -408,24 +413,26 @@ func renderClientTwoFactorPage(w http.ResponseWriter, error string) {
 		CSRFToken:   createCSRFToken(),
 		CSRFToken:   createCSRFToken(),
 		StaticURL:   webStaticFilesPath,
 		StaticURL:   webStaticFilesPath,
 		RecoveryURL: webClientTwoFactorRecoveryPath,
 		RecoveryURL: webClientTwoFactorRecoveryPath,
+		ExtraCSS:    s.binding.ExtraCSS,
 	}
 	}
 	renderClientTemplate(w, templateTwoFactor, data)
 	renderClientTemplate(w, templateTwoFactor, data)
 }
 }
 
 
-func renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error string) {
+func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error string) {
 	data := twoFactorPage{
 	data := twoFactorPage{
 		CurrentURL: webClientTwoFactorRecoveryPath,
 		CurrentURL: webClientTwoFactorRecoveryPath,
 		Version:    version.Get().Version,
 		Version:    version.Get().Version,
 		Error:      error,
 		Error:      error,
 		CSRFToken:  createCSRFToken(),
 		CSRFToken:  createCSRFToken(),
 		StaticURL:  webStaticFilesPath,
 		StaticURL:  webStaticFilesPath,
+		ExtraCSS:   s.binding.ExtraCSS,
 	}
 	}
 	renderClientTemplate(w, templateTwoFactorRecovery, data)
 	renderClientTemplate(w, templateTwoFactorRecovery, data)
 }
 }
 
 
-func renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
 	data := clientMFAPage{
 	data := clientMFAPage{
-		baseClientPage:  getBaseClientPageData(pageMFATitle, webClientMFAPath, r),
+		baseClientPage:  s.getBaseClientPageData(pageMFATitle, webClientMFAPath, r),
 		TOTPConfigs:     mfa.GetAvailableTOTPConfigNames(),
 		TOTPConfigs:     mfa.GetAvailableTOTPConfigNames(),
 		GenerateTOTPURL: webClientTOTPGeneratePath,
 		GenerateTOTPURL: webClientTOTPGeneratePath,
 		ValidateTOTPURL: webClientTOTPValidatePath,
 		ValidateTOTPURL: webClientTOTPValidatePath,
@@ -435,16 +442,16 @@ func renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	user, err := dataprovider.UserExists(data.LoggedUser.Username)
 	user, err := dataprovider.UserExists(data.LoggedUser.Username)
 	if err != nil {
 	if err != nil {
-		renderInternalServerErrorPage(w, r, err)
+		s.renderInternalServerErrorPage(w, r, err)
 		return
 		return
 	}
 	}
 	data.TOTPConfig = user.Filters.TOTPConfig
 	data.TOTPConfig = user.Filters.TOTPConfig
 	renderClientTemplate(w, templateClientMFA, data)
 	renderClientTemplate(w, templateClientMFA, data)
 }
 }
 
 
-func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {
+func (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {
 	data := editFilePage{
 	data := editFilePage{
-		baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
+		baseClientPage: s.getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
 		Path:           fileName,
 		Path:           fileName,
 		Name:           path.Base(fileName),
 		Name:           path.Base(fileName),
 		CurrentDir:     path.Dir(fileName),
 		CurrentDir:     path.Dir(fileName),
@@ -456,7 +463,7 @@ func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileDa
 	renderClientTemplate(w, templateClientEditFile, data)
 	renderClientTemplate(w, templateClientEditFile, data)
 }
 }
 
 
-func renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,
+func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,
 	error string, isAdd bool) {
 	error string, isAdd bool) {
 	currentURL := webClientSharePath
 	currentURL := webClientSharePath
 	title := "Add a new share"
 	title := "Add a new share"
@@ -465,7 +472,7 @@ func renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dat
 		title = "Update share"
 		title = "Update share"
 	}
 	}
 	data := clientSharePage{
 	data := clientSharePage{
-		baseClientPage: getBaseClientPageData(title, currentURL, r),
+		baseClientPage: s.getBaseClientPageData(title, currentURL, r),
 		Share:          share,
 		Share:          share,
 		Error:          error,
 		Error:          error,
 		IsAdd:          isAdd,
 		IsAdd:          isAdd,
@@ -495,10 +502,12 @@ func getDirMapping(dirName, baseWebPath string) []dirMapping {
 	return paths
 	return paths
 }
 }
 
 
-func renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, share dataprovider.Share) {
+func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string,
+	share dataprovider.Share,
+) {
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse")
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse")
 	data := shareFilesPage{
 	data := shareFilesPage{
-		baseClientPage: getBaseClientPageData(pageExtShareTitle, currentURL, r),
+		baseClientPage: s.getBaseClientPageData(pageExtShareTitle, currentURL, r),
 		CurrentDir:     url.QueryEscape(dirName),
 		CurrentDir:     url.QueryEscape(dirName),
 		DirsURL:        path.Join(webClientPubSharesPath, share.ShareID, "dirs"),
 		DirsURL:        path.Join(webClientPubSharesPath, share.ShareID, "dirs"),
 		FilesURL:       currentURL,
 		FilesURL:       currentURL,
@@ -509,21 +518,21 @@ func renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, erro
 	renderClientTemplate(w, templateShareFiles, data)
 	renderClientTemplate(w, templateShareFiles, data)
 }
 }
 
 
-func renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
+func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
 	currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
 	data := shareUploadPage{
 	data := shareUploadPage{
-		baseClientPage: getBaseClientPageData(pageUploadToShareTitle, currentURL, r),
+		baseClientPage: s.getBaseClientPageData(pageUploadToShareTitle, currentURL, r),
 		Share:          &share,
 		Share:          &share,
 		UploadBasePath: path.Join(webClientPubSharesPath, share.ShareID),
 		UploadBasePath: path.Join(webClientPubSharesPath, share.ShareID),
 	}
 	}
 	renderClientTemplate(w, templateUploadToShare, data)
 	renderClientTemplate(w, templateUploadToShare, data)
 }
 }
 
 
-func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
+func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
 	hasIntegrations bool,
 	hasIntegrations bool,
 ) {
 ) {
 	data := filesPage{
 	data := filesPage{
-		baseClientPage:  getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r),
+		baseClientPage:  s.getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r),
 		Error:           error,
 		Error:           error,
 		CurrentDir:      url.QueryEscape(dirName),
 		CurrentDir:      url.QueryEscape(dirName),
 		DownloadURL:     webClientDownloadZipPath,
 		DownloadURL:     webClientDownloadZipPath,
@@ -542,14 +551,14 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
 	renderClientTemplate(w, templateClientFiles, data)
 	renderClientTemplate(w, templateClientFiles, data)
 }
 }
 
 
-func renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) {
+func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) {
 	data := clientProfilePage{
 	data := clientProfilePage{
-		baseClientPage: getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
+		baseClientPage: s.getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
 		Error:          error,
 		Error:          error,
 	}
 	}
 	user, err := dataprovider.UserExists(data.LoggedUser.Username)
 	user, err := dataprovider.UserExists(data.LoggedUser.Username)
 	if err != nil {
 	if err != nil {
-		renderClientInternalServerErrorPage(w, r, err)
+		s.renderClientInternalServerErrorPage(w, r, err)
 		return
 		return
 	}
 	}
 	data.PublicKeys = user.PublicKeys
 	data.PublicKeys = user.PublicKeys
@@ -560,26 +569,26 @@ func renderClientProfilePage(w http.ResponseWriter, r *http.Request, error strin
 	renderClientTemplate(w, templateClientProfile, data)
 	renderClientTemplate(w, templateClientProfile, data)
 }
 }
 
 
-func renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
+func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
 	data := changeClientPasswordPage{
 	data := changeClientPasswordPage{
-		baseClientPage: getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r),
+		baseClientPage: s.getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r),
 		Error:          error,
 		Error:          error,
 	}
 	}
 
 
 	renderClientTemplate(w, templateClientChangePwd, data)
 	renderClientTemplate(w, templateClientChangePwd, data)
 }
 }
 
 
-func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
+		s.renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
 		return
 		return
 	}
 	}
 
 
 	user, err := dataprovider.UserExists(claims.Username)
 	user, err := dataprovider.UserExists(claims.Username)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
+		s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
 		return
 		return
 	}
 	}
 
 
@@ -587,7 +596,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
 	protocol := getProtocolFromRequest(r)
 	protocol := getProtocolFromRequest(r)
 	connectionID := fmt.Sprintf("%v_%v", protocol, connID)
 	connectionID := fmt.Sprintf("%v_%v", protocol, connID)
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	connection := &Connection{
 	connection := &Connection{
@@ -603,7 +612,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
 	var filesList []string
 	var filesList []string
 	err = json.Unmarshal([]byte(files), &filesList)
 	err = json.Unmarshal([]byte(files), &filesList)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "")
+		s.renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "")
 		return
 		return
 	}
 	}
 
 
@@ -611,19 +620,19 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
 	renderCompressedFiles(w, connection, name, filesList, nil)
 	renderCompressedFiles(w, connection, name, filesList, nil)
 }
 }
 
 
-func handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
 	if err := validateBrowsableShare(share, connection); err != nil {
 	if err := validateBrowsableShare(share, connection); err != nil {
-		renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
+		s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
 		return
 		return
 	}
 	}
 	name, err := getBrowsableSharedPath(share, r)
 	name, err := getBrowsableSharedPath(share, r)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
+		s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
 		return
 		return
 	}
 	}
 	common.Connections.Add(connection)
 	common.Connections.Add(connection)
@@ -657,28 +666,28 @@ func handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
 	render.JSON(w, r, results)
 	render.JSON(w, r, results)
 }
 }
 
 
-func handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	share, _, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, true)
+	share, _, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, true)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
-	renderUploadToSharePage(w, r, share)
+	s.renderUploadToSharePage(w, r, share)
 }
 }
 
 
-func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
+	share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
 	if err := validateBrowsableShare(share, connection); err != nil {
 	if err := validateBrowsableShare(share, connection); err != nil {
-		renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
+		s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
 		return
 		return
 	}
 	}
 	name, err := getBrowsableSharedPath(share, r)
 	name, err := getBrowsableSharedPath(share, r)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
+		s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
 		return
 		return
 	}
 	}
 
 
@@ -692,11 +701,11 @@ func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
 		info, err = connection.Stat(name, 1)
 		info, err = connection.Stat(name, 1)
 	}
 	}
 	if err != nil {
 	if err != nil {
-		renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
+		s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
 		return
 		return
 	}
 	}
 	if info.IsDir() {
 	if info.IsDir() {
-		renderSharedFilesPage(w, r, share.GetRelativePath(name), "", share)
+		s.renderSharedFilesPage(w, r, share.GetRelativePath(name), "", share)
 		return
 		return
 	}
 	}
 	inline := r.URL.Query().Get("inline") != ""
 	inline := r.URL.Query().Get("inline") != ""
@@ -704,7 +713,7 @@ func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
 	if status, err := downloadFile(w, r, connection, name, info, inline, &share); err != nil {
 	if status, err := downloadFile(w, r, connection, name, info, inline, &share); err != nil {
 		dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
 		dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
 		if status > 0 {
 		if status > 0 {
-			renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
+			s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
 		}
 		}
 	}
 	}
 }
 }
@@ -787,13 +796,13 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 
 
 	user, err := dataprovider.UserExists(claims.Username)
 	user, err := dataprovider.UserExists(claims.Username)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
+		s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
 		return
 		return
 	}
 	}
 
 
@@ -801,7 +810,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
 	protocol := getProtocolFromRequest(r)
 	protocol := getProtocolFromRequest(r)
 	connectionID := fmt.Sprintf("%v_%v", protocol, connID)
 	connectionID := fmt.Sprintf("%v_%v", protocol, connID)
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	connection := &Connection{
 	connection := &Connection{
@@ -820,37 +829,37 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
 		info, err = connection.Stat(name, 0)
 		info, err = connection.Stat(name, 0)
 	}
 	}
 	if err != nil {
 	if err != nil {
-		renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err),
+		s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err),
 			user, len(s.binding.WebClientIntegrations) > 0)
 			user, len(s.binding.WebClientIntegrations) > 0)
 		return
 		return
 	}
 	}
 	if info.IsDir() {
 	if info.IsDir() {
-		renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0)
+		s.renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0)
 		return
 		return
 	}
 	}
 	inline := r.URL.Query().Get("inline") != ""
 	inline := r.URL.Query().Get("inline") != ""
 	if status, err := downloadFile(w, r, connection, name, info, inline, nil); err != nil && status != 0 {
 	if status, err := downloadFile(w, r, connection, name, info, inline, nil); err != nil && status != 0 {
 		if status > 0 {
 		if status > 0 {
 			if status == http.StatusRequestedRangeNotSatisfiable {
 			if status == http.StatusRequestedRangeNotSatisfiable {
-				renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
+				s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
 				return
 				return
 			}
 			}
-			renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0)
+			s.renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0)
 		}
 		}
 	}
 	}
 }
 }
 
 
-func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 
 
 	user, err := dataprovider.UserExists(claims.Username)
 	user, err := dataprovider.UserExists(claims.Username)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
+		s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
 		return
 		return
 	}
 	}
 
 
@@ -858,7 +867,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
 	protocol := getProtocolFromRequest(r)
 	protocol := getProtocolFromRequest(r)
 	connectionID := fmt.Sprintf("%v_%v", protocol, connID)
 	connectionID := fmt.Sprintf("%v_%v", protocol, connID)
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
 	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	connection := &Connection{
 	connection := &Connection{
@@ -872,24 +881,24 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
 	name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
 	name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
 	info, err := connection.Stat(name, 0)
 	info, err := connection.Stat(name, 0)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %#v", name), "",
+		s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %#v", name), "",
 			getRespStatus(err), nil, "")
 			getRespStatus(err), nil, "")
 		return
 		return
 	}
 	}
 	if info.IsDir() {
 	if info.IsDir() {
-		renderClientMessagePage(w, r, fmt.Sprintf("The path %#v does not point to a file", name), "",
+		s.renderClientMessagePage(w, r, fmt.Sprintf("The path %#v does not point to a file", name), "",
 			http.StatusBadRequest, nil, "")
 			http.StatusBadRequest, nil, "")
 		return
 		return
 	}
 	}
 	if info.Size() > httpdMaxEditFileSize {
 	if info.Size() > httpdMaxEditFileSize {
-		renderClientMessagePage(w, r, fmt.Sprintf("The file size %v for %#v exceeds the maximum allowed size",
+		s.renderClientMessagePage(w, r, fmt.Sprintf("The file size %v for %#v exceeds the maximum allowed size",
 			util.ByteCountIEC(info.Size()), name), "", http.StatusBadRequest, nil, "")
 			util.ByteCountIEC(info.Size()), name), "", http.StatusBadRequest, nil, "")
 		return
 		return
 	}
 	}
 
 
 	reader, err := connection.getFileReader(name, 0, r.Method)
 	reader, err := connection.getFileReader(name, 0, r.Method)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %#v", name), "",
+		s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %#v", name), "",
 			getRespStatus(err), nil, "")
 			getRespStatus(err), nil, "")
 		return
 		return
 	}
 	}
@@ -898,15 +907,15 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
 	var b bytes.Buffer
 	var b bytes.Buffer
 	_, err = io.Copy(&b, reader)
 	_, err = io.Copy(&b, reader)
 	if err != nil {
 	if err != nil {
-		renderClientMessagePage(w, r, fmt.Sprintf("Unable to read the file %#v", name), "", http.StatusInternalServerError,
+		s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to read the file %#v", name), "", http.StatusInternalServerError,
 			nil, "")
 			nil, "")
 		return
 		return
 	}
 	}
 
 
-	renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient))
+	s.renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient))
 }
 }
 
 
-func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	share := &dataprovider.Share{Scope: dataprovider.ShareScopeRead}
 	share := &dataprovider.Share{Scope: dataprovider.ShareScopeRead}
 	dirName := "/"
 	dirName := "/"
@@ -919,7 +928,7 @@ func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
 		var filesList []string
 		var filesList []string
 		err := json.Unmarshal([]byte(files), &filesList)
 		err := json.Unmarshal([]byte(files), &filesList)
 		if err != nil {
 		if err != nil {
-			renderClientMessagePage(w, r, "Invalid share list", "", http.StatusBadRequest, err, "")
+			s.renderClientMessagePage(w, r, "Invalid share list", "", http.StatusBadRequest, err, "")
 			return
 			return
 		}
 		}
 		for _, f := range filesList {
 		for _, f := range filesList {
@@ -929,42 +938,42 @@ func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 
 
-	renderAddUpdateSharePage(w, r, share, "", true)
+	s.renderAddUpdateSharePage(w, r, share, "", true)
 }
 }
 
 
-func handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 	shareID := getURLParam(r, "id")
 	shareID := getURLParam(r, "id")
 	share, err := dataprovider.ShareExists(shareID, claims.Username)
 	share, err := dataprovider.ShareExists(shareID, claims.Username)
 	if err == nil {
 	if err == nil {
 		share.HideConfidentialData()
 		share.HideConfidentialData()
-		renderAddUpdateSharePage(w, r, &share, "", false)
+		s.renderAddUpdateSharePage(w, r, &share, "", false)
 	} else if _, ok := err.(*util.RecordNotFoundError); ok {
 	} else if _, ok := err.(*util.RecordNotFoundError); ok {
-		renderClientNotFoundPage(w, r, err)
+		s.renderClientNotFoundPage(w, r, err)
 	} else {
 	} else {
-		renderClientInternalServerErrorPage(w, r, err)
+		s.renderClientInternalServerErrorPage(w, r, err)
 	}
 	}
 }
 }
 
 
-func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 	share, err := getShareFromPostFields(r)
 	share, err := getShareFromPostFields(r)
 	if err != nil {
 	if err != nil {
-		renderAddUpdateSharePage(w, r, share, err.Error(), true)
+		s.renderAddUpdateSharePage(w, r, share, err.Error(), true)
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	share.ID = 0
 	share.ID = 0
@@ -973,7 +982,7 @@ func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
 	share.Username = claims.Username
 	share.Username = claims.Username
 	if share.Password == "" {
 	if share.Password == "" {
 		if util.IsStringInSlice(sdk.WebClientShareNoPasswordDisabled, claims.Permissions) {
 		if util.IsStringInSlice(sdk.WebClientShareNoPasswordDisabled, claims.Permissions) {
-			renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
+			s.renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
 			return
 			return
 		}
 		}
 	}
 	}
@@ -981,33 +990,33 @@ func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
 	if err == nil {
 	if err == nil {
 		http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
 		http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
 	} else {
 	} else {
-		renderAddUpdateSharePage(w, r, share, err.Error(), true)
+		s.renderAddUpdateSharePage(w, r, share, err.Error(), true)
 	}
 	}
 }
 }
 
 
-func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 	shareID := getURLParam(r, "id")
 	shareID := getURLParam(r, "id")
 	share, err := dataprovider.ShareExists(shareID, claims.Username)
 	share, err := dataprovider.ShareExists(shareID, claims.Username)
 	if _, ok := err.(*util.RecordNotFoundError); ok {
 	if _, ok := err.(*util.RecordNotFoundError); ok {
-		renderClientNotFoundPage(w, r, err)
+		s.renderClientNotFoundPage(w, r, err)
 		return
 		return
 	} else if err != nil {
 	} else if err != nil {
-		renderClientInternalServerErrorPage(w, r, err)
+		s.renderClientInternalServerErrorPage(w, r, err)
 		return
 		return
 	}
 	}
 	updatedShare, err := getShareFromPostFields(r)
 	updatedShare, err := getShareFromPostFields(r)
 	if err != nil {
 	if err != nil {
-		renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
+		s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	updatedShare.ShareID = shareID
 	updatedShare.ShareID = shareID
@@ -1017,7 +1026,7 @@ func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	if updatedShare.Password == "" {
 	if updatedShare.Password == "" {
 		if util.IsStringInSlice(sdk.WebClientShareNoPasswordDisabled, claims.Permissions) {
 		if util.IsStringInSlice(sdk.WebClientShareNoPasswordDisabled, claims.Permissions) {
-			renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
+			s.renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
 			return
 			return
 		}
 		}
 	}
 	}
@@ -1025,15 +1034,15 @@ func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
 	if err == nil {
 	if err == nil {
 		http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
 		http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
 	} else {
 	} else {
-		renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
+		s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
 	}
 	}
 }
 }
 
 
-func handleClientGetShares(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 	limit := defaultQueryLimit
 	limit := defaultQueryLimit
@@ -1046,57 +1055,57 @@ func handleClientGetShares(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	shares := make([]dataprovider.Share, 0, limit)
 	shares := make([]dataprovider.Share, 0, limit)
 	for {
 	for {
-		s, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username)
+		sh, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username)
 		if err != nil {
 		if err != nil {
-			renderInternalServerErrorPage(w, r, err)
+			s.renderInternalServerErrorPage(w, r, err)
 			return
 			return
 		}
 		}
-		shares = append(shares, s...)
-		if len(s) < limit {
+		shares = append(shares, sh...)
+		if len(sh) < limit {
 			break
 			break
 		}
 		}
 	}
 	}
 	data := clientSharesPage{
 	data := clientSharesPage{
-		baseClientPage:      getBaseClientPageData(pageClientSharesTitle, webClientSharesPath, r),
+		baseClientPage:      s.getBaseClientPageData(pageClientSharesTitle, webClientSharesPath, r),
 		Shares:              shares,
 		Shares:              shares,
 		BasePublicSharesURL: webClientPubSharesPath,
 		BasePublicSharesURL: webClientPubSharesPath,
 	}
 	}
 	renderClientTemplate(w, templateClientShares, data)
 	renderClientTemplate(w, templateClientShares, data)
 }
 }
 
 
-func handleClientGetProfile(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientGetProfile(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	renderClientProfilePage(w, r, "")
+	s.renderClientProfilePage(w, r, "")
 }
 }
 
 
-func handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	renderClientChangePasswordPage(w, r, "")
+	s.renderClientChangePasswordPage(w, r, "")
 }
 }
 
 
-func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderClientProfilePage(w, r, err.Error())
+		s.renderClientProfilePage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	claims, err := getTokenClaims(r)
 	claims, err := getTokenClaims(r)
 	if err != nil || claims.Username == "" {
 	if err != nil || claims.Username == "" {
-		renderClientForbiddenPage(w, r, "Invalid token claims")
+		s.renderClientForbiddenPage(w, r, "Invalid token claims")
 		return
 		return
 	}
 	}
 	user, err := dataprovider.UserExists(claims.Username)
 	user, err := dataprovider.UserExists(claims.Username)
 	if err != nil {
 	if err != nil {
-		renderClientProfilePage(w, r, err.Error())
+		s.renderClientProfilePage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
 	if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
-		renderClientForbiddenPage(w, r, "You are not allowed to change anything")
+		s.renderClientForbiddenPage(w, r, "You are not allowed to change anything")
 		return
 		return
 	}
 	}
 	if user.CanManagePublicKeys() {
 	if user.CanManagePublicKeys() {
@@ -1111,26 +1120,26 @@ func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 	err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
 	if err != nil {
 	if err != nil {
-		renderClientProfilePage(w, r, err.Error())
+		s.renderClientProfilePage(w, r, err.Error())
 		return
 		return
 	}
 	}
-	renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
+	s.renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
 		"Your profile has been successfully updated")
 		"Your profile has been successfully updated")
 }
 }
 
 
-func handleWebClientMFA(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	renderClientMFAPage(w, r)
+	s.renderClientMFAPage(w, r)
 }
 }
 
 
-func handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	renderClientTwoFactorPage(w, "")
+	s.renderClientTwoFactorPage(w, "")
 }
 }
 
 
-func handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	renderClientTwoFactorRecoveryPage(w, "")
+	s.renderClientTwoFactorRecoveryPage(w, "")
 }
 }
 
 
 func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
 func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
@@ -1166,53 +1175,53 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
 	return share, nil
 	return share, nil
 }
 }
 
 
-func handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	if !smtp.IsEnabled() {
 	if !smtp.IsEnabled() {
-		renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
+		s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 		return
 	}
 	}
-	renderClientForgotPwdPage(w, "")
+	s.renderClientForgotPwdPage(w, "")
 }
 }
 
 
-func handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
 	err := r.ParseForm()
 	err := r.ParseForm()
 	if err != nil {
 	if err != nil {
-		renderClientForgotPwdPage(w, err.Error())
+		s.renderClientForgotPwdPage(w, err.Error())
 		return
 		return
 	}
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
-		renderClientForbiddenPage(w, r, err.Error())
+		s.renderClientForbiddenPage(w, r, err.Error())
 		return
 		return
 	}
 	}
 	username := r.Form.Get("username")
 	username := r.Form.Get("username")
 	err = handleForgotPassword(r, username, false)
 	err = handleForgotPassword(r, username, false)
 	if err != nil {
 	if err != nil {
 		if e, ok := err.(*util.ValidationError); ok {
 		if e, ok := err.(*util.ValidationError); ok {
-			renderClientForgotPwdPage(w, e.GetErrorString())
+			s.renderClientForgotPwdPage(w, e.GetErrorString())
 			return
 			return
 		}
 		}
-		renderClientForgotPwdPage(w, err.Error())
+		s.renderClientForgotPwdPage(w, err.Error())
 		return
 		return
 	}
 	}
 	http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
 	http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
 }
 }
 
 
-func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	if !smtp.IsEnabled() {
 	if !smtp.IsEnabled() {
-		renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
+		s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 		return
 	}
 	}
-	renderClientResetPwdPage(w, "")
+	s.renderClientResetPwdPage(w, "")
 }
 }
 
 
-func handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
+func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
 	name := r.URL.Query().Get("path")
 	name := r.URL.Query().Get("path")
 	if name == "" {
 	if name == "" {
-		renderClientBadRequestPage(w, r, errors.New("no file specified"))
+		s.renderClientBadRequestPage(w, r, errors.New("no file specified"))
 		return
 		return
 	}
 	}
 	name = util.CleanPath(name)
 	name = util.CleanPath(name)
@@ -1220,6 +1229,7 @@ func handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
 		Title:     path.Base(name),
 		Title:     path.Base(name),
 		URL:       fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)),
 		URL:       fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)),
 		StaticURL: webStaticFilesPath,
 		StaticURL: webStaticFilesPath,
+		ExtraCSS:  s.binding.ExtraCSS,
 	}
 	}
 	renderClientTemplate(w, templateClientViewPDF, data)
 	renderClientTemplate(w, templateClientViewPDF, data)
 }
 }

+ 1 - 1
pkgs/build.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 #!/bin/bash
 
 
-NFPM_VERSION=2.14.0
+NFPM_VERSION=2.15.0
 NFPM_ARCH=${NFPM_ARCH:-amd64}
 NFPM_ARCH=${NFPM_ARCH:-amd64}
 if [ -z ${SFTPGO_VERSION} ]
 if [ -z ${SFTPGO_VERSION} ]
 then
 then

+ 2 - 1
sftpgo.json

@@ -250,7 +250,8 @@
           "permissions_policy": "",
           "permissions_policy": "",
           "cross_origin_opener_policy": "",
           "cross_origin_opener_policy": "",
           "expect_ct_header": ""
           "expect_ct_header": ""
-        }
+        },
+        "extra_css": []
       }
       }
     ],
     ],
     "templates_path": "templates",
     "templates_path": "templates",

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

@@ -70,6 +70,10 @@
         }
         }
     </style>
     </style>
 
 
+    {{range .ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{end}}
+
 </head>
 </head>
 
 
 <body class="bg-gradient-primary">
 <body class="bg-gradient-primary">

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

@@ -70,6 +70,10 @@
         }
         }
     </style>
     </style>
 
 
+    {{range .ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{end}}
+
 </head>
 </head>
 
 
 <body class="bg-gradient-primary">
 <body class="bg-gradient-primary">

+ 4 - 0
templates/webadmin/base.html

@@ -53,6 +53,10 @@
     </style>
     </style>
     {{block "extra_css" .}}{{end}}
     {{block "extra_css" .}}{{end}}
 
 
+    {{range .ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{end}}
+
 </head>
 </head>
 
 
 <body id="page-top">
 <body id="page-top">

+ 4 - 0
templates/webadmin/baselogin.html

@@ -71,6 +71,10 @@
         }
         }
     </style>
     </style>
 
 
+    {{range .ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{end}}
+
 </head>
 </head>
 
 
 <body class="bg-gradient-primary">
 <body class="bg-gradient-primary">

+ 4 - 0
templates/webclient/base.html

@@ -53,6 +53,10 @@
     </style>
     </style>
     {{block "extra_css" .}}{{end}}
     {{block "extra_css" .}}{{end}}
 
 
+    {{range .ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{end}}
+
 </head>
 </head>
 
 
 <body id="page-top">
 <body id="page-top">

+ 4 - 0
templates/webclient/baselogin.html

@@ -71,6 +71,10 @@
         }
         }
     </style>
     </style>
 
 
+    {{range .ExtraCSS}}
+    <link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
+    {{end}}
+
 </head>
 </head>
 
 
 <body class="bg-gradient-primary">
 <body class="bg-gradient-primary">

+ 4 - 0
templates/webclient/viewpdf.html

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

Some files were not shown because too many files changed in this diff