web UIs: add branding support

Fixes #829

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-05-13 19:40:52 +02:00
parent 4bea9ed760
commit 5d7f6960f3
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
26 changed files with 318 additions and 128 deletions

View file

@ -111,7 +111,7 @@ var (
CrossOriginOpenerPolicy: "", CrossOriginOpenerPolicy: "",
ExpectCTHeader: "", ExpectCTHeader: "",
}, },
ExtraCSS: []httpd.CustomCSS{}, Branding: httpd.Branding{},
} }
defaultRateLimiter = common.RateLimiterConfig{ defaultRateLimiter = common.RateLimiterConfig{
Average: 0, Average: 0,
@ -1294,23 +1294,78 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) {
return result, isSet return result, isSet
} }
func getHTTPDExtraCSSFromEnv(idx int) []httpd.CustomCSS { func getHTTPDUIBrandingFromEnv(prefix string) (httpd.UIBranding, bool) {
var css []httpd.CustomCSS var result httpd.UIBranding
isSet := false
for subIdx := 0; subIdx < 10; subIdx++ { name, ok := os.LookupEnv(fmt.Sprintf("%s__NAME", prefix))
var customCSS httpd.CustomCSS
path, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__EXTRA_CSS__%v__PATH", idx, subIdx))
if ok { if ok {
customCSS.Path = path result.Name = name
isSet = true
} }
if path != "" { shortName, ok := os.LookupEnv(fmt.Sprintf("%s__SHORT_NAME", prefix))
css = append(css, customCSS) if ok {
} result.ShortName = shortName
isSet = true
} }
return css faviconPath, ok := os.LookupEnv(fmt.Sprintf("%s__FAVICON_PATH", prefix))
if ok {
result.FaviconPath = faviconPath
isSet = true
}
logoPath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGO_PATH", prefix))
if ok {
result.LogoPath = logoPath
isSet = true
}
loginImagePath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGIN_IMAGE_PATH", prefix))
if ok {
result.LoginImagePath = loginImagePath
isSet = true
}
disclaimerName, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_NAME", prefix))
if ok {
result.DisclaimerName = disclaimerName
isSet = true
}
disclaimerPath, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_PATH", prefix))
if ok {
result.DisclaimerPath = disclaimerPath
isSet = true
}
extraCSS, ok := lookupStringListFromEnv(fmt.Sprintf("%s__EXTRA_CSS", prefix))
if ok {
result.ExtraCSS = extraCSS
isSet = true
}
return result, isSet
}
func getHTTPDBrandingFromEnv(idx int) (httpd.Branding, bool) {
var result httpd.Branding
isSet := false
webAdmin, ok := getHTTPDUIBrandingFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__BRANDING__WEB_ADMIN", idx))
if ok {
result.WebAdmin = webAdmin
isSet = true
}
webClient, ok := getHTTPDUIBrandingFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__BRANDING__WEB_CLIENT", idx))
if ok {
result.WebClient = webClient
isSet = true
}
return result, isSet
} }
func getHTTPDWebClientIntegrationsFromEnv(idx int) []httpd.WebClientIntegration { func getHTTPDWebClientIntegrationsFromEnv(idx int) []httpd.WebClientIntegration {
@ -1372,10 +1427,9 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
isSet = true isSet = true
} }
extraCSS := getHTTPDExtraCSSFromEnv(idx) brandingConf, ok := getHTTPDBrandingFromEnv(idx)
if len(extraCSS) > 0 { if ok {
binding.ExtraCSS = extraCSS binding.Branding = brandingConf
isSet = true
} }
return isSet return isSet

View file

@ -828,7 +828,8 @@ 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__1__BRANDING__WEB_ADMIN__NAME", "Web Admin")
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_CLIENT__SHORT_NAME", "WebClient")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS", "127.0.1.1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__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")
@ -869,6 +870,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER", `max-age=86400, enforce, report-uri="https://foo.example/report"`) os.Setenv("SFTPGO_HTTPD__BINDINGS__2__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__0__PATH", "path1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH", "logo.png")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH", "login_image.png")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME", "disclaimer")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS", "1.css,2.css")
t.Cleanup(func() { 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")
@ -877,6 +884,8 @@ 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__BRANDING__WEB_ADMIN__NAME")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__BRANDING__WEB_CLIENT__SHORT_NAME")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__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")
@ -918,6 +927,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS")
}) })
configDir := ".." configDir := ".."
@ -941,7 +956,6 @@ 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)
@ -949,7 +963,8 @@ 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, "Web Admin", bindings[1].Branding.WebAdmin.Name)
require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName)
require.Equal(t, 9000, bindings[2].Port) require.Equal(t, 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)
@ -997,9 +1012,14 @@ 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, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
require.Equal(t, "path1", bindings[2].ExtraCSS[0].Path) require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
require.Equal(t, "path2", bindings[2].ExtraCSS[1].Path) require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath)
require.Equal(t, "disclaimer", bindings[2].Branding.WebClient.DisclaimerName)
require.Equal(t, "disclaimer.html", bindings[2].Branding.WebAdmin.DisclaimerPath)
require.Len(t, bindings[2].Branding.WebClient.ExtraCSS, 2)
require.Equal(t, "1.css", bindings[2].Branding.WebClient.ExtraCSS[0])
require.Equal(t, "2.css", bindings[2].Branding.WebClient.ExtraCSS[1])
} }
func TestHTTPClientCertificatesFromEnv(t *testing.T) { func TestHTTPClientCertificatesFromEnv(t *testing.T) {

View file

@ -272,8 +272,15 @@ 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: - `branding`, struct. Defines the supported customizations to suit your brand. It contains the `web_admin` and `web_client` structs that define customizations for the WebAdmin and the WebClient UIs. Each customization struct contains the following fields:
- `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. - `name`, string. Defines the UI name
- `short_name`, string. Define the short name to show next to the logo image
- `favicon_path`, string. Path to the favicon relative to `static_files_path`. For example, if you create a directory named `branding` inside the static dir and put the `favicon.ico` file in it, you must set `/branding/favicon.ico` as path.
- `logo_path`, string. Path to your logo relative to `static_files_path`. The preferred image size is 256x256 pixel
- `login_image_path`, string. Path to a custom image to show on the login screen relative to `static_files_path`. The preferred image size is 900x900 pixel
- `disclaimer_name`, string. Name for your optional disclaimer
- `disclaimer_path`, string. Path to the HTML page with the disclaimer relative to `static_files_path`
- `extra_css`, list of strings. Defines the paths, relative to `static_files_path`, to additional CSS files
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir - `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

View file

@ -314,12 +314,56 @@ func (s *SecurityConf) getHTTPSProxyHeaders() map[string]string {
return headers return headers
} }
// CustomCSS defines the configuration for custom CSS // UIBranding defines the supported customizations for the web UIs
type CustomCSS struct { type UIBranding struct {
// Path to the CSS file relative to "static_files_path". // Name defines the text to show at the login page and as HTML title
// For example, if you create a directory named "extra_css" inside the static dir Name string `json:"name" mapstructure:"name"`
// and put the "my.css" file in it, you must set "/extra_css/my.css" as path. // ShortName defines the name to show next to the logo image
Path string `json:"path" mapstructure:"path"` ShortName string `json:"short_name" mapstructure:"short_name"`
// Path to your logo relative to "static_files_path".
// For example, if you create a directory named "branding" inside the static dir and
// put the "mylogo.png" file in it, you must set "/branding/mylogo.png" as logo path.
LogoPath string `json:"logo_path" mapstructure:"logo_path"`
// Path to the image to show on the login screen relative to "static_files_path"
LoginImagePath string `json:"login_image_path" mapstructure:"login_image_path"`
// Path to your favicon relative to "static_files_path"
FaviconPath string `json:"favicon_path" mapstructure:"favicon_path"`
// DisclaimerName defines the name for the link to your optional disclaimer
DisclaimerName string `json:"disclaimer_name" mapstructure:"disclaimer_name"`
// Path to the HTML page for your disclaimer relative to "static_files_path".
DisclaimerPath string `json:"disclaimer_path" mapstructure:"disclaimer_path"`
// Additional CSS file paths, relative to "static_files_path", to include
ExtraCSS []string `json:"extra_css" mapstructure:"extra_css"`
}
func (b *UIBranding) check() {
if b.LogoPath != "" {
b.LogoPath = util.CleanPath(b.LogoPath)
} else {
b.LogoPath = "/img/logo.png"
}
if b.LoginImagePath != "" {
b.LoginImagePath = util.CleanPath(b.LoginImagePath)
} else {
b.LoginImagePath = "/img/login_image.png"
}
if b.FaviconPath != "" {
b.FaviconPath = util.CleanPath(b.FaviconPath)
} else {
b.FaviconPath = "/favicon.ico"
}
if b.DisclaimerPath != "" {
b.DisclaimerPath = util.CleanPath(b.DisclaimerPath)
}
for idx := range b.ExtraCSS {
b.ExtraCSS[idx] = util.CleanPath(b.ExtraCSS[idx])
}
}
// Branding defines the branding-related customizations supported
type Branding struct {
WebAdmin UIBranding `json:"web_admin" mapstructure:"web_admin"`
WebClient UIBranding `json:"web_client" mapstructure:"web_client"`
} }
// WebClientIntegration defines the configuration for an external Web Client integration // WebClientIntegration defines the configuration for an external Web Client integration
@ -379,8 +423,8 @@ type Binding struct {
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 // Branding defines customizations to suit your brand
ExtraCSS []CustomCSS `json:"extra_css" mapstructure:"extra_css"` Branding Branding `json:"branding" mapstructure:"branding"`
allowHeadersFrom []func(net.IP) bool allowHeadersFrom []func(net.IP) bool
} }
@ -394,14 +438,21 @@ func (b *Binding) checkWebClientIntegrations() {
b.WebClientIntegrations = integrations b.WebClientIntegrations = integrations
} }
func (b *Binding) checkExtraCSS() { func (b *Binding) checkBranding() {
var extraCSS []CustomCSS b.Branding.WebAdmin.check()
for _, css := range b.ExtraCSS { b.Branding.WebClient.check()
extraCSS = append(extraCSS, CustomCSS{ if b.Branding.WebAdmin.Name == "" {
Path: path.Join("/", css.Path), b.Branding.WebAdmin.Name = "SFTPGo WebAdmin"
}) }
if b.Branding.WebAdmin.ShortName == "" {
b.Branding.WebAdmin.ShortName = "WebAdmin"
}
if b.Branding.WebClient.Name == "" {
b.Branding.WebClient.Name = "SFTPGo WebClient"
}
if b.Branding.WebClient.ShortName == "" {
b.Branding.WebClient.ShortName = "WebClient"
} }
b.ExtraCSS = extraCSS
} }
func (b *Binding) parseAllowedProxy() error { func (b *Binding) parseAllowedProxy() error {
@ -642,7 +693,7 @@ func (c *Conf) Initialize(configDir string) error {
return err return err
} }
binding.checkWebClientIntegrations() binding.checkWebClientIntegrations()
binding.checkExtraCSS() binding.checkBranding()
binding.Security.updateProxyHeaders() binding.Security.updateProxyHeaders()
go func(b Binding) { go func(b Binding) {

View file

@ -301,21 +301,31 @@ func TestShouldBind(t *testing.T) {
} }
} }
func TestExtraCSSValidation(t *testing.T) { func TestBrandingValidation(t *testing.T) {
b := Binding{ b := Binding{
ExtraCSS: []CustomCSS{ Branding: Branding{
{ WebAdmin: UIBranding{
Path: "path1", LogoPath: "path1",
LoginImagePath: "login1.png",
}, },
{ WebClient: UIBranding{
Path: "../path2", FaviconPath: "favicon1.ico",
DisclaimerPath: "../path2",
ExtraCSS: []string{"1.css"},
}, },
}, },
} }
b.checkExtraCSS() b.checkBranding()
require.Len(t, b.ExtraCSS, 2) assert.Equal(t, "/favicon.ico", b.Branding.WebAdmin.FaviconPath)
assert.Equal(t, "/path1", b.ExtraCSS[0].Path) assert.Equal(t, "/path1", b.Branding.WebAdmin.LogoPath)
assert.Equal(t, "/path2", b.ExtraCSS[1].Path) assert.Equal(t, "/login1.png", b.Branding.WebAdmin.LoginImagePath)
assert.Len(t, b.Branding.WebAdmin.ExtraCSS, 0)
assert.Equal(t, "/favicon1.ico", b.Branding.WebClient.FaviconPath)
assert.Equal(t, "/path2", b.Branding.WebClient.DisclaimerPath)
assert.Equal(t, "/img/login_image.png", b.Branding.WebClient.LoginImagePath)
if assert.Len(t, b.Branding.WebClient.ExtraCSS, 1) {
assert.Equal(t, "/1.css", b.Branding.WebClient.ExtraCSS[0])
}
} }
func TestRedactedConf(t *testing.T) { func TestRedactedConf(t *testing.T) {

View file

@ -140,7 +140,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error, ip str
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebClient,
} }
if s.binding.showAdminLoginURL() { if s.binding.showAdminLoginURL() {
data.AltLoginURL = webAdminLoginPath data.AltLoginURL = webAdminLoginPath
@ -516,7 +516,7 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, error, ip stri
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebAdmin,
} }
if s.binding.showClientLoginURL() { if s.binding.showClientLoginURL() {
data.AltLoginURL = webClientLoginPath data.AltLoginURL = webClientLoginPath

View file

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

View file

@ -131,7 +131,7 @@ type basePage struct {
HasDefender bool HasDefender bool
HasExternalLogin bool HasExternalLogin bool
LoggedAdmin *dataprovider.Admin LoggedAdmin *dataprovider.Admin
ExtraCSS []CustomCSS Branding UIBranding
} }
type usersPage struct { type usersPage struct {
@ -465,7 +465,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
HasDefender: common.Config.DefenderConfig.Enabled, HasDefender: common.Config.DefenderConfig.Enabled,
HasExternalLogin: isLoggedInWithOIDC(r), HasExternalLogin: isLoggedInWithOIDC(r),
CSRFToken: csrfToken, CSRFToken: csrfToken,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebAdmin,
} }
} }
@ -518,7 +518,7 @@ func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, error, ip strin
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
Title: pageForgotPwdTitle, Title: pageForgotPwdTitle,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebAdmin,
} }
renderAdminTemplate(w, templateForgotPassword, data) renderAdminTemplate(w, templateForgotPassword, data)
} }
@ -530,7 +530,7 @@ func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, error, ip string
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
Title: pageResetPwdTitle, Title: pageResetPwdTitle,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebAdmin,
} }
renderAdminTemplate(w, templateResetPassword, data) renderAdminTemplate(w, templateResetPassword, data)
} }
@ -543,7 +543,7 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, error, ip strin
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
RecoveryURL: webAdminTwoFactorRecoveryPath, RecoveryURL: webAdminTwoFactorRecoveryPath,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebAdmin,
} }
renderAdminTemplate(w, templateTwoFactor, data) renderAdminTemplate(w, templateTwoFactor, data)
} }
@ -555,7 +555,7 @@ func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, error,
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebAdmin,
} }
renderAdminTemplate(w, templateTwoFactorRecovery, data) renderAdminTemplate(w, templateTwoFactorRecovery, data)
} }

View file

@ -98,7 +98,7 @@ type baseClientPage struct {
CSRFToken string CSRFToken string
HasExternalLogin bool HasExternalLogin bool
LoggedUser *dataprovider.User LoggedUser *dataprovider.User
ExtraCSS []CustomCSS Branding UIBranding
} }
type dirMapping struct { type dirMapping struct {
@ -110,7 +110,7 @@ type viewPDFPage struct {
Title string Title string
URL string URL string
StaticURL string StaticURL string
ExtraCSS []CustomCSS Branding UIBranding
} }
type editFilePage struct { type editFilePage struct {
@ -339,7 +339,7 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
CSRFToken: csrfToken, CSRFToken: csrfToken,
HasExternalLogin: isLoggedInWithOIDC(r), HasExternalLogin: isLoggedInWithOIDC(r),
LoggedUser: getUserFromToken(r), LoggedUser: getUserFromToken(r),
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebClient,
} }
} }
@ -350,7 +350,7 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error, ip
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
Title: pageClientForgotPwdTitle, Title: pageClientForgotPwdTitle,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebClient,
} }
renderClientTemplate(w, templateForgotPassword, data) renderClientTemplate(w, templateForgotPassword, data)
} }
@ -362,7 +362,7 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error, ip
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
Title: pageClientResetPwdTitle, Title: pageClientResetPwdTitle,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebClient,
} }
renderClientTemplate(w, templateResetPassword, data) renderClientTemplate(w, templateResetPassword, data)
} }
@ -415,7 +415,7 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error, ip
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
RecoveryURL: webClientTwoFactorRecoveryPath, RecoveryURL: webClientTwoFactorRecoveryPath,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebClient,
} }
renderClientTemplate(w, templateTwoFactor, data) renderClientTemplate(w, templateTwoFactor, data)
} }
@ -427,7 +427,7 @@ func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, e
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
ExtraCSS: s.binding.ExtraCSS, Branding: s.binding.Branding.WebClient,
} }
renderClientTemplate(w, templateTwoFactorRecovery, data) renderClientTemplate(w, templateTwoFactorRecovery, data)
} }
@ -1263,7 +1263,7 @@ func (s *httpdServer) 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, Branding: s.binding.Branding.WebClient,
} }
renderClientTemplate(w, templateClientViewPDF, data) renderClientTemplate(w, templateClientViewPDF, data)
} }

View file

@ -7001,9 +7001,9 @@ func TestHashedPasswords(t *testing.T) {
user.Password = clearPwd user.Password = clearPwd
conn, client, err := getSftpClient(user, usePubKey) conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err, "unable to login with password %#v", pwd) { if assert.NoError(t, err, "unable to login with password %#v", pwd) {
defer conn.Close()
defer client.Close()
assert.NoError(t, checkBasicSFTP(client)) assert.NoError(t, checkBasicSFTP(client))
conn.Close()
client.Close()
} }
user.Password = pwd user.Password = pwd
conn, client, err = getSftpClient(user, usePubKey) conn, client, err = getSftpClient(user, usePubKey)
@ -7026,9 +7026,9 @@ func TestHashedPasswords(t *testing.T) {
user.Password = clearPwd user.Password = clearPwd
conn, client, err = getSftpClient(user, usePubKey) conn, client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err, "unable to login with password %#v", pwd) { if assert.NoError(t, err, "unable to login with password %#v", pwd) {
defer conn.Close()
defer client.Close()
assert.NoError(t, checkBasicSFTP(client)) assert.NoError(t, checkBasicSFTP(client))
conn.Close()
client.Close()
} }
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -258,7 +258,28 @@
"cross_origin_opener_policy": "", "cross_origin_opener_policy": "",
"expect_ct_header": "" "expect_ct_header": ""
}, },
"branding": {
"web_admin": {
"name": "",
"short_name": "",
"favicon_path": "",
"logo_path": "",
"login_image_path": "",
"disclaimer_name": "",
"disclaimer_path": "",
"extra_css": [] "extra_css": []
},
"web_client": {
"name": "",
"short_name": "",
"favicon_path": "",
"logo_path": "",
"login_image_path": "",
"disclaimer_name": "",
"disclaimer_path": "",
"extra_css": []
}
}
} }
], ],
"templates_path": "templates", "templates_path": "templates",

View file

@ -1,10 +0,0 @@
.bg-login-image {
background-image: url(/static/img/logo.png);
background-size: contain;
background-repeat: no-repeat;
padding: 1em;
}
.row.login-image {
height: 200px;
}

BIN
static/img/login_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,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}} {{range .Branding.ExtraCSS}}
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css"> <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
{{end}} {{end}}
</head> </head>