mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-24 16:40:26 +00:00
allow to customize name and log from the WebUI
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
b2926377b7
commit
b5c821795a
16 changed files with 996 additions and 101 deletions
14
go.mod
14
go.mod
|
@ -73,13 +73,13 @@ require (
|
|||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.188.0
|
||||
google.golang.org/api v0.189.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/auth v0.7.1 // indirect
|
||||
cloud.google.com/go/auth v0.7.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.11 // indirect
|
||||
|
@ -116,9 +116,9 @@ require (
|
|||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
|
@ -173,9 +173,9 @@ require (
|
|||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto v0.0.0-20240723171418-e6d459c13d2a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
36
go.sum
36
go.sum
|
@ -1,18 +1,18 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s=
|
||||
cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
||||
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
|
||||
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw=
|
||||
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ=
|
||||
cloud.google.com/go/kms v1.18.2 h1:EGgD0B9k9tOOkbPhYW1PHo2W0teamAUYMOUIcDRMfPk=
|
||||
cloud.google.com/go/kms v1.18.2/go.mod h1:YFz1LYrnGsXARuRePL729oINmN5J/5e7nYijgvfiIeY=
|
||||
cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k=
|
||||
cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
|
||||
cloud.google.com/go/kms v1.18.3 h1:8+Z2S4bQDSCdghB5ZA5dVDDJTLmnkRlowtFiXqMFd74=
|
||||
cloud.google.com/go/kms v1.18.3/go.mod h1:y/Lcf6fyhbdn7MrG1VaDqXxM8rhOBc5rWcWAhcvZjQU=
|
||||
cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E=
|
||||
cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics=
|
||||
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
|
@ -195,8 +195,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -207,8 +207,8 @@ github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
|||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
@ -514,19 +514,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
|
||||
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
|
||||
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
|
||||
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
|
||||
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto v0.0.0-20240723171418-e6d459c13d2a h1:hPbLwHFm59QoSKUT0uGaL19YN4U9W5lY4+iNXlUBNj0=
|
||||
google.golang.org/genproto v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:+7gIV7FP6jBo5hiY2lsWA//NkNORQVj0J1Isc/4HzR4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a h1:YIa/rzVqMEokBkPtydCkx1VLmv3An1Uw7w1P1m6EhOY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
|
|
|
@ -163,8 +163,14 @@ var (
|
|||
rateLimiters map[string][]*rateLimiter
|
||||
isShuttingDown atomic.Bool
|
||||
ftpLoginCommands = []string{"PASS", "USER"}
|
||||
fnUpdateBranding func(*dataprovider.BrandingConfigs)
|
||||
)
|
||||
|
||||
// SetUpdateBrandingFn sets the function to call to update branding configs.
|
||||
func SetUpdateBrandingFn(fn func(*dataprovider.BrandingConfigs)) {
|
||||
fnUpdateBranding = fn
|
||||
}
|
||||
|
||||
// Initialize sets the common configuration
|
||||
func Initialize(c Configuration, isShared int) error {
|
||||
isShuttingDown.Store(false)
|
||||
|
@ -403,6 +409,23 @@ func AddDefenderEvent(ip, protocol string, event HostEvent) bool {
|
|||
return Config.defender.AddEvent(ip, protocol, event)
|
||||
}
|
||||
|
||||
func reloadProviderConfigs() {
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to load config from provider: %v", err)
|
||||
return
|
||||
}
|
||||
configs.SetNilsToEmpty()
|
||||
if fnUpdateBranding != nil {
|
||||
fnUpdateBranding(configs.Branding)
|
||||
}
|
||||
if err := configs.SMTP.TryDecrypt(); err != nil {
|
||||
logger.Error(logSender, "", "unable to decrypt smtp config: %v", err)
|
||||
return
|
||||
}
|
||||
smtp.Activate(configs.SMTP)
|
||||
}
|
||||
|
||||
func startPeriodicChecks(duration time.Duration, isShared int) {
|
||||
startEventScheduler()
|
||||
spec := fmt.Sprintf("@every %s", duration)
|
||||
|
@ -411,7 +434,7 @@ func startPeriodicChecks(duration time.Duration, isShared int) {
|
|||
logger.Info(logSender, "", "scheduled overquota transfers check, schedule %q", spec)
|
||||
if isShared == 1 {
|
||||
logger.Info(logSender, "", "add reload configs task")
|
||||
_, err := eventScheduler.AddFunc("@every 10m", smtp.ReloadProviderConf)
|
||||
_, err := eventScheduler.AddFunc("@every 10m", reloadProviderConfigs)
|
||||
util.PanicOnError(err)
|
||||
}
|
||||
if Config.IdleTimeout > 0 {
|
||||
|
|
|
@ -15,8 +15,11 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
|
@ -305,6 +308,27 @@ func (c *SMTPConfigs) TryDecrypt() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) prepareForRendering() {
|
||||
if c.Password != nil {
|
||||
c.Password.Hide()
|
||||
if c.Password.IsEmpty() {
|
||||
c.Password = nil
|
||||
}
|
||||
}
|
||||
if c.OAuth2.ClientSecret != nil {
|
||||
c.OAuth2.ClientSecret.Hide()
|
||||
if c.OAuth2.ClientSecret.IsEmpty() {
|
||||
c.OAuth2.ClientSecret = nil
|
||||
}
|
||||
}
|
||||
if c.OAuth2.RefreshToken != nil {
|
||||
c.OAuth2.RefreshToken.Hide()
|
||||
if c.OAuth2.RefreshToken.IsEmpty() {
|
||||
c.OAuth2.RefreshToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
|
||||
var password *kms.Secret
|
||||
if c.Password != nil {
|
||||
|
@ -387,13 +411,137 @@ func (c *ACMEConfigs) getACopy() *ACMEConfigs {
|
|||
}
|
||||
}
|
||||
|
||||
// BrandingConfig defines the branding configuration
|
||||
type BrandingConfig struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Logo []byte `json:"logo"`
|
||||
Favicon []byte `json:"favicon"`
|
||||
DisclaimerName string `json:"disclaimer_name"`
|
||||
DisclaimerURL string `json:"disclaimer_url"`
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) isEmpty() bool {
|
||||
if c.Name != "" {
|
||||
return false
|
||||
}
|
||||
if c.ShortName != "" {
|
||||
return false
|
||||
}
|
||||
if len(c.Logo) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(c.Favicon) > 0 {
|
||||
return false
|
||||
}
|
||||
if c.DisclaimerName != "" && c.DisclaimerURL != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (*BrandingConfig) validatePNG(b []byte, maxWidth, maxHeight int) error {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
// DecodeConfig is more efficient, but I'm not sure if this would lead to
|
||||
// accepting invalid images in some edge cases and performance does not
|
||||
// matter here.
|
||||
img, err := png.Decode(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid PNG image"),
|
||||
util.I18nErrorInvalidPNG,
|
||||
)
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid PNG image size"),
|
||||
util.I18nErrorInvalidPNGSize,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) validateDisclaimerURL() error {
|
||||
if c.DisclaimerURL == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := url.Parse(c.DisclaimerURL)
|
||||
if err != nil {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid disclaimer URL"),
|
||||
util.I18nErrorInvalidDisclaimerURL,
|
||||
)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("invalid disclaimer URL scheme"),
|
||||
util.I18nErrorInvalidDisclaimerURL,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) validate() error {
|
||||
if err := c.validateDisclaimerURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.validatePNG(c.Logo, 512, 512); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.validatePNG(c.Favicon, 256, 256)
|
||||
}
|
||||
|
||||
func (c *BrandingConfig) getACopy() BrandingConfig {
|
||||
logo := make([]byte, len(c.Logo))
|
||||
copy(logo, c.Logo)
|
||||
favicon := make([]byte, len(c.Favicon))
|
||||
copy(favicon, c.Favicon)
|
||||
|
||||
return BrandingConfig{
|
||||
Name: c.Name,
|
||||
ShortName: c.ShortName,
|
||||
Logo: logo,
|
||||
Favicon: favicon,
|
||||
DisclaimerName: c.DisclaimerName,
|
||||
DisclaimerURL: c.DisclaimerURL,
|
||||
}
|
||||
}
|
||||
|
||||
// BrandingConfigs defines the branding configuration for WebAdmin and WebClient UI
|
||||
type BrandingConfigs struct {
|
||||
WebAdmin BrandingConfig
|
||||
WebClient BrandingConfig
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) isEmpty() bool {
|
||||
return c.WebAdmin.isEmpty() && c.WebClient.isEmpty()
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) validate() error {
|
||||
if err := c.WebAdmin.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.WebClient.validate()
|
||||
}
|
||||
|
||||
func (c *BrandingConfigs) getACopy() *BrandingConfigs {
|
||||
return &BrandingConfigs{
|
||||
WebAdmin: c.WebAdmin.getACopy(),
|
||||
WebClient: c.WebClient.getACopy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Configs allows to set configuration keys disabled by default without
|
||||
// modifying the config file or setting env vars
|
||||
type Configs struct {
|
||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||
ACME *ACMEConfigs `json:"acme,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
|
||||
SMTP *SMTPConfigs `json:"smtp,omitempty"`
|
||||
ACME *ACMEConfigs `json:"acme,omitempty"`
|
||||
Branding *BrandingConfigs `json:"branding,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Configs) validate() error {
|
||||
|
@ -412,6 +560,11 @@ func (c *Configs) validate() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if c.Branding != nil {
|
||||
if err := c.Branding.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -428,25 +581,11 @@ func (c *Configs) PrepareForRendering() {
|
|||
if c.ACME != nil && c.ACME.isEmpty() {
|
||||
c.ACME = nil
|
||||
}
|
||||
if c.Branding != nil && c.Branding.isEmpty() {
|
||||
c.Branding = nil
|
||||
}
|
||||
if c.SMTP != nil {
|
||||
if c.SMTP.Password != nil {
|
||||
c.SMTP.Password.Hide()
|
||||
if c.SMTP.Password.IsEmpty() {
|
||||
c.SMTP.Password = nil
|
||||
}
|
||||
}
|
||||
if c.SMTP.OAuth2.ClientSecret != nil {
|
||||
c.SMTP.OAuth2.ClientSecret.Hide()
|
||||
if c.SMTP.OAuth2.ClientSecret.IsEmpty() {
|
||||
c.SMTP.OAuth2.ClientSecret = nil
|
||||
}
|
||||
}
|
||||
if c.SMTP.OAuth2.RefreshToken != nil {
|
||||
c.SMTP.OAuth2.RefreshToken.Hide()
|
||||
if c.SMTP.OAuth2.RefreshToken.IsEmpty() {
|
||||
c.SMTP.OAuth2.RefreshToken = nil
|
||||
}
|
||||
}
|
||||
c.SMTP.prepareForRendering()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -470,6 +609,9 @@ func (c *Configs) SetNilsToEmpty() {
|
|||
if c.ACME == nil {
|
||||
c.ACME = &ACMEConfigs{}
|
||||
}
|
||||
if c.Branding == nil {
|
||||
c.Branding = &BrandingConfigs{}
|
||||
}
|
||||
}
|
||||
|
||||
// RenderAsJSON implements the renderer interface used within plugins
|
||||
|
@ -498,6 +640,9 @@ func (c *Configs) getACopy() Configs {
|
|||
if c.ACME != nil {
|
||||
result.ACME = c.ACME.getACopy()
|
||||
}
|
||||
if c.Branding != nil {
|
||||
result.Branding = c.Branding.getACopy()
|
||||
}
|
||||
result.UpdatedAt = c.UpdatedAt
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -363,6 +363,16 @@ func streamJSONArray(w http.ResponseWriter, chunkSize int, dataGetter func(limit
|
|||
streamData(w, []byte("]"))
|
||||
}
|
||||
|
||||
func renderPNGImage(w http.ResponseWriter, r *http.Request, b []byte) {
|
||||
if len(b) == 0 {
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusNotFound)
|
||||
render.PlainText(w, r.WithContext(ctx), http.StatusText(http.StatusNotFound))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
streamData(w, b)
|
||||
}
|
||||
|
||||
func getCompressedFileName(username string, files []string) string {
|
||||
if len(files) == 1 {
|
||||
name := path.Base(files[0])
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
@ -285,12 +286,88 @@ var (
|
|||
installationCodeHint string
|
||||
fnInstallationCodeResolver FnInstallationCodeResolver
|
||||
configurationDir string
|
||||
dbBrandingConfig brandingCache
|
||||
)
|
||||
|
||||
func init() {
|
||||
updateWebAdminURLs("")
|
||||
updateWebClientURLs("")
|
||||
acme.SetReloadHTTPDCertsFn(ReloadCertificateMgr)
|
||||
common.SetUpdateBrandingFn(dbBrandingConfig.Set)
|
||||
}
|
||||
|
||||
type brandingCache struct {
|
||||
mu sync.RWMutex
|
||||
configs *dataprovider.BrandingConfigs
|
||||
}
|
||||
|
||||
func (b *brandingCache) Set(configs *dataprovider.BrandingConfigs) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.configs = configs
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebAdminLogo() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebAdmin.Logo
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebAdminFavicon() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebAdmin.Favicon
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebClientLogo() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebClient.Logo
|
||||
}
|
||||
|
||||
func (b *brandingCache) getWebClientFavicon() []byte {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
return b.configs.WebClient.Favicon
|
||||
}
|
||||
|
||||
func (b *brandingCache) mergeBrandingConfig(branding UIBranding, isWebClient bool) UIBranding {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
var urlPrefix string
|
||||
var cfg dataprovider.BrandingConfig
|
||||
if isWebClient {
|
||||
cfg = b.configs.WebClient
|
||||
urlPrefix = "webclient"
|
||||
} else {
|
||||
cfg = b.configs.WebAdmin
|
||||
urlPrefix = "webadmin"
|
||||
}
|
||||
if cfg.Name != "" {
|
||||
branding.Name = cfg.Name
|
||||
}
|
||||
if cfg.ShortName != "" {
|
||||
branding.ShortName = cfg.ShortName
|
||||
}
|
||||
if cfg.DisclaimerName != "" {
|
||||
branding.DisclaimerName = cfg.DisclaimerName
|
||||
}
|
||||
if cfg.DisclaimerURL != "" {
|
||||
branding.DisclaimerPath = cfg.DisclaimerURL
|
||||
}
|
||||
if len(cfg.Logo) > 0 {
|
||||
branding.LogoPath = path.Join("/", "branding", urlPrefix, "logo.png")
|
||||
}
|
||||
if len(cfg.Favicon) > 0 {
|
||||
branding.FaviconPath = path.Join("/", "branding", urlPrefix, "favicon.png")
|
||||
}
|
||||
return branding
|
||||
}
|
||||
|
||||
// FnInstallationCodeResolver defines a method to get the installation code.
|
||||
|
@ -406,19 +483,23 @@ type UIBranding struct {
|
|||
// the default CSS files
|
||||
DefaultCSS []string `json:"default_css" mapstructure:"default_css"`
|
||||
// Additional CSS file paths, relative to "static_files_path", to include
|
||||
ExtraCSS []string `json:"extra_css" mapstructure:"extra_css"`
|
||||
ExtraCSS []string `json:"extra_css" mapstructure:"extra_css"`
|
||||
DefaultLogoPath string `json:"-" mapstructure:"-"`
|
||||
DefaultFaviconPath string `json:"-" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (b *UIBranding) check() {
|
||||
b.DefaultLogoPath = "/img/logo.png"
|
||||
b.DefaultFaviconPath = "/favicon.png"
|
||||
if b.LogoPath != "" {
|
||||
b.LogoPath = util.CleanPath(b.LogoPath)
|
||||
} else {
|
||||
b.LogoPath = "/img/logo.png"
|
||||
b.LogoPath = b.DefaultLogoPath
|
||||
}
|
||||
if b.FaviconPath != "" {
|
||||
b.FaviconPath = util.CleanPath(b.FaviconPath)
|
||||
} else {
|
||||
b.FaviconPath = "/favicon.png"
|
||||
b.FaviconPath = b.DefaultFaviconPath
|
||||
}
|
||||
if b.DisclaimerPath != "" {
|
||||
if !strings.HasPrefix(b.DisclaimerPath, "https://") && !strings.HasPrefix(b.DisclaimerPath, "http://") {
|
||||
|
@ -548,6 +629,14 @@ func (b *Binding) checkBranding() {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *Binding) webAdminBranding() UIBranding {
|
||||
return dbBrandingConfig.mergeBrandingConfig(b.Branding.WebAdmin, false)
|
||||
}
|
||||
|
||||
func (b *Binding) webClientBranding() UIBranding {
|
||||
return dbBrandingConfig.mergeBrandingConfig(b.Branding.WebClient, true)
|
||||
}
|
||||
|
||||
func (b *Binding) parseAllowedProxy() error {
|
||||
if filepath.IsAbs(b.Address) && len(b.ProxyAllowed) > 0 {
|
||||
// unix domain socket
|
||||
|
@ -879,6 +968,7 @@ func (c *Conf) loadFromProvider() error {
|
|||
return fmt.Errorf("unable to load config from provider: %w", err)
|
||||
}
|
||||
configs.SetNilsToEmpty()
|
||||
dbBrandingConfig.Set(configs.Branding)
|
||||
if configs.ACME.Domain == "" || !configs.ACME.HasProtocol(common.ProtocolHTTP) {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -20,6 +20,9 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
|
@ -13350,10 +13353,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form := make(url.Values)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err := getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
// parse form error
|
||||
|
@ -13371,10 +13376,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Add("sftp_host_key_algos", ssh.InsecureCertAlgoDSAv01)
|
||||
form.Set("sftp_pub_key_algos", ssh.InsecureKeyAlgoDSA)
|
||||
form.Set("form_action", "sftp_submit")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nError500Message) // invalid algo
|
||||
|
@ -13383,10 +13390,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Set("sftp_pub_key_algos", ssh.InsecureKeyAlgoDSA)
|
||||
form.Set("sftp_kex_algos", "diffie-hellman-group18-sha512")
|
||||
form.Add("sftp_kex_algos", ssh.KeyExchangeDH16SHA512)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
|
@ -13401,10 +13410,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
assert.Contains(t, configs.SFTPD.KexAlgorithms, ssh.KeyExchangeDH16SHA512)
|
||||
// invalid form action
|
||||
form.Set("form_action", "")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nError400Message)
|
||||
|
@ -13416,10 +13427,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Set("smtp_password", defaultPassword)
|
||||
form.Set("smtp_domain", "localdomain")
|
||||
form.Set("smtp_auth", "100")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nError500Message) // invalid smtp_auth
|
||||
|
@ -13430,10 +13443,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Set("smtp_debug", "checked")
|
||||
form.Set("smtp_oauth2_provider", "1")
|
||||
form.Set("smtp_oauth2_client_id", "123")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
|
@ -13460,10 +13475,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Set("smtp_password", redactedSecret)
|
||||
form.Set("smtp_auth", "")
|
||||
configs.SMTP.AuthType = 0 // empty will be converted to 0
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
|
@ -13479,10 +13496,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
updatedConfigs.SMTP.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, encryptedPayload, secretKey, "")
|
||||
err = dataprovider.UpdateConfigs(&updatedConfigs, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
|
@ -13492,19 +13511,23 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Add("acme_protocols", "2")
|
||||
form.Add("acme_protocols", "3")
|
||||
form.Set("acme_domain", "example.com")
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
// no email set, validation will fail
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidEmail)
|
||||
form.Set("acme_domain", "")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
|
@ -13535,10 +13558,12 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
form.Add("acme_protocols", "1000")
|
||||
form.Set("acme_domain", domain)
|
||||
form.Set("acme_email", "email@example.com")
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
|
@ -13562,6 +13587,201 @@ func TestWebConfigsMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBrandingConfigMock(t *testing.T) {
|
||||
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
webClientLogoPath := "/static/branding/webclient/logo.png"
|
||||
webClientFaviconPath := "/static/branding/webclient/favicon.png"
|
||||
webAdminLogoPath := "/static/branding/webadmin/logo.png"
|
||||
webAdminFaviconPath := "/static/branding/webadmin/favicon.png"
|
||||
// no custom log or favicon was set
|
||||
for _, p := range []string{webClientLogoPath, webClientFaviconPath, webAdminLogoPath, webAdminFaviconPath} {
|
||||
req, err := http.NewRequest(http.MethodGet, p, nil)
|
||||
assert.NoError(t, err)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFTokenFromInternalPageMock(webConfigsPath, webToken)
|
||||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("form_action", "branding_submit")
|
||||
form.Set("branding_webadmin_name", "Custom WebAdmin")
|
||||
form.Set("branding_webadmin_short_name", "WebAdmin")
|
||||
form.Set("branding_webadmin_disclaimer_name", "Admin disclaimer")
|
||||
form.Set("branding_webadmin_disclaimer_url", "invalid, not a URL")
|
||||
form.Set("branding_webclient_name", "Custom WebClient")
|
||||
form.Set("branding_webclient_short_name", "WebClient")
|
||||
form.Set("branding_webclient_disclaimer_name", "Client disclaimer")
|
||||
form.Set("branding_webclient_disclaimer_url", "https://example.com")
|
||||
b, contentType, err := getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidDisclaimerURL)
|
||||
|
||||
form.Set("branding_webadmin_disclaimer_url", "https://example.net")
|
||||
tmpFile := filepath.Join(os.TempDir(), util.GenerateUniqueID()+".png")
|
||||
err = createTestPNG(tmpFile, 512, 512, color.RGBA{100, 200, 200, 0xff})
|
||||
assert.NoError(t, err)
|
||||
|
||||
b, contentType, err = getMultipartFormData(form, "branding_webadmin_logo", tmpFile)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
// check
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Custom WebAdmin", configs.Branding.WebAdmin.Name)
|
||||
assert.Equal(t, "WebAdmin", configs.Branding.WebAdmin.ShortName)
|
||||
assert.Equal(t, "Admin disclaimer", configs.Branding.WebAdmin.DisclaimerName)
|
||||
assert.Equal(t, "https://example.net", configs.Branding.WebAdmin.DisclaimerURL)
|
||||
assert.Equal(t, "Custom WebClient", configs.Branding.WebClient.Name)
|
||||
assert.Equal(t, "WebClient", configs.Branding.WebClient.ShortName)
|
||||
assert.Equal(t, "Client disclaimer", configs.Branding.WebClient.DisclaimerName)
|
||||
assert.Equal(t, "https://example.com", configs.Branding.WebClient.DisclaimerURL)
|
||||
assert.Greater(t, len(configs.Branding.WebAdmin.Logo), 0)
|
||||
assert.Len(t, configs.Branding.WebAdmin.Favicon, 0)
|
||||
assert.Len(t, configs.Branding.WebClient.Logo, 0)
|
||||
assert.Len(t, configs.Branding.WebClient.Favicon, 0)
|
||||
|
||||
err = createTestPNG(tmpFile, 256, 256, color.RGBA{120, 220, 220, 0xff})
|
||||
assert.NoError(t, err)
|
||||
form.Set("branding_webadmin_logo_remove", "0") // 0 preserves WebAdmin logo
|
||||
b, contentType, err = getMultipartFormData(form, "branding_webadmin_favicon", tmpFile)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Custom WebAdmin", configs.Branding.WebAdmin.Name)
|
||||
assert.Equal(t, "WebAdmin", configs.Branding.WebAdmin.ShortName)
|
||||
assert.Equal(t, "Admin disclaimer", configs.Branding.WebAdmin.DisclaimerName)
|
||||
assert.Equal(t, "https://example.net", configs.Branding.WebAdmin.DisclaimerURL)
|
||||
assert.Equal(t, "Custom WebClient", configs.Branding.WebClient.Name)
|
||||
assert.Equal(t, "WebClient", configs.Branding.WebClient.ShortName)
|
||||
assert.Equal(t, "Client disclaimer", configs.Branding.WebClient.DisclaimerName)
|
||||
assert.Equal(t, "https://example.com", configs.Branding.WebClient.DisclaimerURL)
|
||||
assert.Greater(t, len(configs.Branding.WebAdmin.Logo), 0)
|
||||
assert.Greater(t, len(configs.Branding.WebAdmin.Favicon), 0)
|
||||
assert.Len(t, configs.Branding.WebClient.Logo, 0)
|
||||
assert.Len(t, configs.Branding.WebClient.Favicon, 0)
|
||||
|
||||
err = createTestPNG(tmpFile, 256, 256, color.RGBA{80, 90, 110, 0xff})
|
||||
assert.NoError(t, err)
|
||||
b, contentType, err = getMultipartFormData(form, "branding_webclient_logo", tmpFile)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(configs.Branding.WebClient.Logo), 0)
|
||||
|
||||
err = createTestPNG(tmpFile, 256, 256, color.RGBA{120, 50, 120, 0xff})
|
||||
assert.NoError(t, err)
|
||||
b, contentType, err = getMultipartFormData(form, "branding_webclient_favicon", tmpFile)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(configs.Branding.WebClient.Favicon), 0)
|
||||
|
||||
for _, p := range []string{webClientLogoPath, webClientFaviconPath, webAdminLogoPath, webAdminFaviconPath} {
|
||||
req, err := http.NewRequest(http.MethodGet, p, nil)
|
||||
assert.NoError(t, err)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
// remove images
|
||||
form.Set("branding_webadmin_logo_remove", "1")
|
||||
form.Set("branding_webclient_logo_remove", "1")
|
||||
form.Set("branding_webadmin_favicon_remove", "1")
|
||||
form.Set("branding_webclient_favicon_remove", "1")
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nConfigsOK)
|
||||
configs, err = dataprovider.GetConfigs()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, configs.Branding.WebAdmin.Logo, 0)
|
||||
assert.Len(t, configs.Branding.WebAdmin.Favicon, 0)
|
||||
assert.Len(t, configs.Branding.WebClient.Logo, 0)
|
||||
assert.Len(t, configs.Branding.WebClient.Favicon, 0)
|
||||
for _, p := range []string{webClientLogoPath, webClientFaviconPath, webAdminLogoPath, webAdminFaviconPath} {
|
||||
req, err := http.NewRequest(http.MethodGet, p, nil)
|
||||
assert.NoError(t, err)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
form.Del("branding_webadmin_logo_remove")
|
||||
form.Del("branding_webclient_logo_remove")
|
||||
form.Del("branding_webadmin_favicon_remove")
|
||||
form.Del("branding_webclient_favicon_remove")
|
||||
// image too large
|
||||
err = createTestPNG(tmpFile, 768, 512, color.RGBA{120, 50, 120, 0xff})
|
||||
assert.NoError(t, err)
|
||||
b, contentType, err = getMultipartFormData(form, "branding_webclient_logo", tmpFile)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidPNGSize)
|
||||
// not a png image
|
||||
err = createTestFile(tmpFile, 128)
|
||||
assert.NoError(t, err)
|
||||
b, contentType, err = getMultipartFormData(form, "branding_webclient_logo", tmpFile)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webConfigsPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidPNG)
|
||||
|
||||
err = os.Remove(tmpFile)
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSFTPLoopError(t *testing.T) {
|
||||
user1 := getTestUser()
|
||||
user2 := getTestUser()
|
||||
|
@ -26798,6 +27018,23 @@ func isDbDefenderSupported() bool {
|
|||
}
|
||||
}
|
||||
|
||||
func createTestPNG(name string, width, height int, imgColor color.Color) error {
|
||||
upLeft := image.Point{0, 0}
|
||||
lowRight := image.Point{width, height}
|
||||
img := image.NewRGBA(image.Rectangle{upLeft, lowRight})
|
||||
for x := 0; x < width; x++ {
|
||||
for y := 0; y < height; y++ {
|
||||
img.Set(x, y, imgColor)
|
||||
}
|
||||
}
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, img)
|
||||
}
|
||||
|
||||
func BenchmarkSecretDecryption(b *testing.B) {
|
||||
s := kms.NewPlainSecret("test data")
|
||||
s.SetAdditionalData("username")
|
||||
|
|
|
@ -412,6 +412,16 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
|
|||
assert.EqualError(t, err, http.ErrNotMultipart.Error())
|
||||
}
|
||||
|
||||
func TestBrandingInvalidFormFile(t *testing.T) {
|
||||
form := make(url.Values)
|
||||
req, _ := http.NewRequest(http.MethodPost, webConfigsPath, strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
err := req.ParseForm()
|
||||
assert.NoError(t, err)
|
||||
_, err = getBrandingConfigFromPostFields(req, &dataprovider.BrandingConfigs{})
|
||||
assert.EqualError(t, err, http.ErrNotMultipart.Error())
|
||||
}
|
||||
|
||||
func TestVerifyCSRFToken(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -172,7 +173,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Reque
|
|||
CurrentURL: webClientLoginPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
FormDisabled: s.binding.isWebClientLoginFormDisabled(),
|
||||
CheckRedirect: true,
|
||||
}
|
||||
|
@ -181,7 +182,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
if s.binding.showAdminLoginURL() {
|
||||
data.AltLoginURL = webAdminLoginPath
|
||||
data.AltLoginName = s.binding.Branding.WebAdmin.ShortName
|
||||
data.AltLoginName = s.binding.webAdminBranding().ShortName
|
||||
}
|
||||
if smtp.IsEnabled() && !data.FormDisabled {
|
||||
data.ForgotPwdURL = webClientForgotPwdPath
|
||||
|
@ -590,13 +591,13 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Reques
|
|||
CurrentURL: webAdminLoginPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseAdminPath),
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
FormDisabled: s.binding.isWebAdminLoginFormDisabled(),
|
||||
CheckRedirect: false,
|
||||
}
|
||||
if s.binding.showClientLoginURL() {
|
||||
data.AltLoginURL = webClientLoginPath
|
||||
data.AltLoginName = s.binding.Branding.WebClient.ShortName
|
||||
data.AltLoginName = s.binding.webClientBranding().ShortName
|
||||
}
|
||||
if smtp.IsEnabled() && !data.FormDisabled {
|
||||
data.ForgotPwdURL = webAdminForgotPwdPath
|
||||
|
@ -1520,6 +1521,16 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
http.Redirect(w, r, webClientLoginPath, http.StatusFound)
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webclient/logo.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebClientLogo())
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webclient/favicon.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebClientFavicon())
|
||||
})
|
||||
s.router.Get(webClientLoginPath, s.handleClientWebLogin)
|
||||
if s.binding.OIDC.isEnabled() && !s.binding.isWebClientOIDCLoginDisabled() {
|
||||
s.router.Get(webClientOIDCLoginPath, s.handleWebClientOIDCLogin)
|
||||
|
@ -1643,6 +1654,16 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
s.redirectToWebPath(w, r, webAdminLoginPath)
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webadmin/logo.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebAdminLogo())
|
||||
})
|
||||
s.router.Get(path.Join(webStaticFilesPath, "branding/webadmin/favicon.png"),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderPNGImage(w, r, dbBrandingConfig.getWebAdminFavicon())
|
||||
})
|
||||
s.router.Get(webAdminLoginPath, s.handleWebAdminLogin)
|
||||
if s.binding.OIDC.hasRoles() && !s.binding.isWebAdminOIDCLoginDisabled() {
|
||||
s.router.Get(webAdminOIDCLoginPath, s.handleWebAdminOIDCLogin)
|
||||
|
|
|
@ -329,6 +329,7 @@ type configsPage struct {
|
|||
RedactedSecret string
|
||||
OAuth2TokenURL string
|
||||
OAuth2RedirectURL string
|
||||
WebClientBranding UIBranding
|
||||
Error *util.I18nError
|
||||
}
|
||||
|
||||
|
@ -662,7 +663,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, w http.ResponseW
|
|||
HasSearcher: plugin.Handler.HasSearcher(),
|
||||
HasExternalLogin: isLoggedInWithOIDC(r),
|
||||
CSRFToken: csrfToken,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -720,7 +721,7 @@ func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request
|
|||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseAdminPath),
|
||||
LoginURL: webAdminLoginPath,
|
||||
Title: util.I18nForgotPwdTitle,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateForgotPassword, data)
|
||||
}
|
||||
|
@ -733,7 +734,7 @@ func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request,
|
|||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath),
|
||||
LoginURL: webAdminLoginPath,
|
||||
Title: util.I18nResetPwdTitle,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateResetPassword, data)
|
||||
}
|
||||
|
@ -746,7 +747,7 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request
|
|||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath),
|
||||
RecoveryURL: webAdminTwoFactorRecoveryPath,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateTwoFactor, data)
|
||||
}
|
||||
|
@ -758,7 +759,7 @@ func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http
|
|||
CurrentURL: webAdminTwoFactorRecoveryPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath),
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
renderAdminTemplate(w, templateTwoFactorRecovery, data)
|
||||
}
|
||||
|
@ -838,6 +839,7 @@ func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request,
|
|||
RedactedSecret: redactedSecret,
|
||||
OAuth2TokenURL: webOAuth2TokenPath,
|
||||
OAuth2RedirectURL: webOAuth2RedirectPath,
|
||||
WebClientBranding: s.binding.webClientBranding(),
|
||||
Error: getI18nError(err),
|
||||
}
|
||||
|
||||
|
@ -855,7 +857,7 @@ func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Reques
|
|||
InstallationCodeHint: installationCodeHint,
|
||||
HideSupportLink: hideSupportLink,
|
||||
Error: err,
|
||||
Branding: s.binding.Branding.WebAdmin,
|
||||
Branding: s.binding.webAdminBranding(),
|
||||
}
|
||||
|
||||
renderAdminTemplate(w, templateSetup, data)
|
||||
|
@ -1582,7 +1584,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
|
|||
config.AutomaticCredentials = 0
|
||||
}
|
||||
credentials, _, err := r.FormFile("gcs_credential_file")
|
||||
if err == http.ErrMissingFile {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
return config, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -2760,6 +2762,71 @@ func getSMTPConfigsFromPostFields(r *http.Request) *dataprovider.SMTPConfigs {
|
|||
}
|
||||
}
|
||||
|
||||
func getImageInputBytes(r *http.Request, fieldName, removeFieldName string, defaultVal []byte) ([]byte, error) {
|
||||
var result []byte
|
||||
remove := r.Form.Get(removeFieldName)
|
||||
if remove == "" || remove == "0" {
|
||||
result = defaultVal
|
||||
}
|
||||
f, _, err := r.FormFile(fieldName)
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
return result, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func getBrandingConfigFromPostFields(r *http.Request, config *dataprovider.BrandingConfigs) (
|
||||
*dataprovider.BrandingConfigs, error,
|
||||
) {
|
||||
if config == nil {
|
||||
config = &dataprovider.BrandingConfigs{}
|
||||
}
|
||||
adminLogo, err := getImageInputBytes(r, "branding_webadmin_logo", "branding_webadmin_logo_remove", config.WebAdmin.Logo)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
adminFavicon, err := getImageInputBytes(r, "branding_webadmin_favicon", "branding_webadmin_favicon_remove",
|
||||
config.WebAdmin.Favicon)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
clientLogo, err := getImageInputBytes(r, "branding_webclient_logo", "branding_webclient_logo_remove",
|
||||
config.WebClient.Logo)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
clientFavicon, err := getImageInputBytes(r, "branding_webclient_favicon", "branding_webclient_favicon_remove",
|
||||
config.WebClient.Favicon)
|
||||
if err != nil {
|
||||
return nil, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
|
||||
branding := &dataprovider.BrandingConfigs{
|
||||
WebAdmin: dataprovider.BrandingConfig{
|
||||
Name: strings.TrimSpace(r.Form.Get("branding_webadmin_name")),
|
||||
ShortName: strings.TrimSpace(r.Form.Get("branding_webadmin_short_name")),
|
||||
Logo: adminLogo,
|
||||
Favicon: adminFavicon,
|
||||
DisclaimerName: strings.TrimSpace(r.Form.Get("branding_webadmin_disclaimer_name")),
|
||||
DisclaimerURL: strings.TrimSpace(r.Form.Get("branding_webadmin_disclaimer_url")),
|
||||
},
|
||||
WebClient: dataprovider.BrandingConfig{
|
||||
Name: strings.TrimSpace(r.Form.Get("branding_webclient_name")),
|
||||
ShortName: strings.TrimSpace(r.Form.Get("branding_webclient_short_name")),
|
||||
Logo: clientLogo,
|
||||
Favicon: clientFavicon,
|
||||
DisclaimerName: strings.TrimSpace(r.Form.Get("branding_webclient_disclaimer_name")),
|
||||
DisclaimerURL: strings.TrimSpace(r.Form.Get("branding_webclient_disclaimer_url")),
|
||||
},
|
||||
}
|
||||
return branding, nil
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
if !smtp.IsEnabled() {
|
||||
|
@ -4207,11 +4274,13 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
|||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
err = r.ParseForm()
|
||||
err = r.ParseMultipartForm(maxRequestSize)
|
||||
if err != nil {
|
||||
s.renderBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
|
||||
return
|
||||
}
|
||||
defer r.MultipartForm.RemoveAll() //nolint:errcheck
|
||||
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := verifyCSRFToken(r, s.csrfTokenAuth); err != nil {
|
||||
s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
|
||||
|
@ -4237,6 +4306,15 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
|||
smtpConfigs := getSMTPConfigsFromPostFields(r)
|
||||
updateSMTPSecrets(smtpConfigs, configs.SMTP)
|
||||
configs.SMTP = smtpConfigs
|
||||
case "branding_submit":
|
||||
configSection = 4
|
||||
brandingConfigs, err := getBrandingConfigFromPostFields(r, configs.Branding)
|
||||
if err != nil {
|
||||
logger.Info(logSender, "", "unable to get branding config: %v", err)
|
||||
s.renderConfigsPage(w, r, configs, err, configSection)
|
||||
return
|
||||
}
|
||||
configs.Branding = brandingConfigs
|
||||
default:
|
||||
s.renderBadRequestPage(w, r, errors.New("unsupported form action"))
|
||||
return
|
||||
|
@ -4247,15 +4325,22 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques
|
|||
s.renderConfigsPage(w, r, configs, err, configSection)
|
||||
return
|
||||
}
|
||||
if configSection == 3 {
|
||||
postConfigsUpdate(configSection, configs)
|
||||
s.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK)
|
||||
}
|
||||
|
||||
func postConfigsUpdate(section int, configs dataprovider.Configs) {
|
||||
switch section {
|
||||
case 3:
|
||||
err := configs.SMTP.TryDecrypt()
|
||||
if err == nil {
|
||||
smtp.Activate(configs.SMTP)
|
||||
} else {
|
||||
logger.Error(logSender, "", "unable to decrypt SMTP configuration, cannot activate configuration: %v", err)
|
||||
}
|
||||
case 4:
|
||||
dbBrandingConfig.Set(configs.Branding)
|
||||
}
|
||||
s.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -546,7 +546,7 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, w http.Res
|
|||
CSRFToken: csrfToken,
|
||||
LoggedUser: getUserFromToken(r),
|
||||
IsLoggedToShare: false,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
if !strings.HasPrefix(r.RequestURI, webClientPubSharesPath) {
|
||||
data.LoginURL = webClientLoginPath
|
||||
|
@ -562,7 +562,7 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.R
|
|||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath),
|
||||
LoginURL: webClientLoginPath,
|
||||
Title: util.I18nForgotPwdTitle,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateForgotPassword, data)
|
||||
}
|
||||
|
@ -575,7 +575,7 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Re
|
|||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath),
|
||||
LoginURL: webClientLoginPath,
|
||||
Title: util.I18nResetPwdTitle,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateResetPassword, data)
|
||||
}
|
||||
|
@ -587,7 +587,7 @@ func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Reques
|
|||
CurrentURL: r.RequestURI,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateShareLogin, data)
|
||||
}
|
||||
|
@ -637,7 +637,7 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R
|
|||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath),
|
||||
RecoveryURL: webClientTwoFactorRecoveryPath,
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
if next := r.URL.Query().Get("next"); strings.HasPrefix(next, webClientFilesPath) {
|
||||
data.CurrentURL += "?next=" + url.QueryEscape(next)
|
||||
|
@ -652,7 +652,7 @@ func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r
|
|||
CurrentURL: webClientTwoFactorRecoveryPath,
|
||||
Error: err,
|
||||
CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateTwoFactorRecovery, data)
|
||||
}
|
||||
|
@ -1102,7 +1102,7 @@ func (s *httpdServer) handleShareViewPDF(w http.ResponseWriter, r *http.Request)
|
|||
Title: path.Base(name),
|
||||
URL: fmt.Sprintf("%s?path=%s&_=%d", path.Join(webClientPubSharesPath, share.ShareID, "getpdf"),
|
||||
url.QueryEscape(name), time.Now().UTC().Unix()),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateClientViewPDF, data)
|
||||
}
|
||||
|
@ -1778,7 +1778,7 @@ func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request
|
|||
commonBasePage: getCommonBasePage(r),
|
||||
Title: path.Base(name),
|
||||
URL: fmt.Sprintf("%s?path=%s&_=%d", webClientGetPDFPath, url.QueryEscape(name), time.Now().UTC().Unix()),
|
||||
Branding: s.binding.Branding.WebClient,
|
||||
Branding: s.binding.webClientBranding(),
|
||||
}
|
||||
renderClientTemplate(w, templateClientViewPDF, data)
|
||||
}
|
||||
|
|
|
@ -415,12 +415,6 @@ func SendEmail(to, bcc []string, subject, body string, contentType EmailContentT
|
|||
return config.sendEmail(to, bcc, subject, body, contentType, attachments...)
|
||||
}
|
||||
|
||||
// ReloadProviderConf reloads the configuration from the provider
|
||||
// and apply it if different from the active one
|
||||
func ReloadProviderConf() {
|
||||
loadConfigFromProvider() //nolint:errcheck
|
||||
}
|
||||
|
||||
func loadConfigFromProvider() error {
|
||||
configs, err := dataprovider.GetConfigs()
|
||||
if err != nil {
|
||||
|
|
|
@ -303,6 +303,9 @@ const (
|
|||
I18nErrorEvSyncUnsupportedFs = "rules.sync_unsupported_fs_event"
|
||||
I18nErrorRuleFailureActionsOnly = "rules.only_failure_actions"
|
||||
I18nErrorRuleSyncActionRequired = "rules.sync_action_required"
|
||||
I18nErrorInvalidPNG = "branding.invalid_png"
|
||||
I18nErrorInvalidPNGSize = "branding.invalid_png_size"
|
||||
I18nErrorInvalidDisclaimerURL = "branding.invalid_disclaimer_url"
|
||||
)
|
||||
|
||||
// NewI18nError returns a I18nError wrappring the provided error
|
||||
|
|
|
@ -883,6 +883,19 @@
|
|||
"help": "From this section you can enable algorithms disabled by default. You don't need to set values already defined using env vars or config file. A service restart is required to apply changes",
|
||||
"host_key_algos": "Host Key Algorithms"
|
||||
},
|
||||
"branding": {
|
||||
"title": "Branding",
|
||||
"help": "From this section you can customize SFTPGo to fit your brand and add a disclaimer to the login pages",
|
||||
"short_name": "Short name",
|
||||
"logo": "Logo",
|
||||
"logo_help": "PNG image, max accepted size 512x512, default logo size is 256x256",
|
||||
"favicon": "Favicon",
|
||||
"disclaimer_name": "Disclaimer title",
|
||||
"disclaimer_url": "Disclaimer URL",
|
||||
"invalid_png": "Invalid PNG image",
|
||||
"invalid_png_size": "Invalid PNG image size",
|
||||
"invalid_disclaimer_url": "The disclaimer URL must be an http or https link"
|
||||
},
|
||||
"events": {
|
||||
"search": "Search logs",
|
||||
"fs_events": "Filesystem events",
|
||||
|
|
|
@ -883,6 +883,19 @@
|
|||
"help": "Da questa sezione è possibile abilitare gli algoritmi disabilitati di default. Non è necessario impostare valori già definiti utilizzando env vars o il file di configurazione. Per applicare le modifiche è necessario il riavvio del servizio",
|
||||
"host_key_algos": "Algoritmi per chiavi host"
|
||||
},
|
||||
"branding": {
|
||||
"title": "Branding",
|
||||
"help": "Da questa sezione puoi personalizzare SFTPGo per adattarlo al tuo marchio e aggiungere un disclaimer alle pagine di accesso",
|
||||
"short_name": "Nome breve",
|
||||
"logo": "Logo",
|
||||
"logo_help": "Immagine PNG, dimensione massima accettata 512x512, la dimensione predefinita del logo è 256x256",
|
||||
"favicon": "Favicon",
|
||||
"disclaimer_name": "Titolo Disclaimer",
|
||||
"disclaimer_url": "URL Disclaimer",
|
||||
"invalid_png": "Immagine PNG non valida",
|
||||
"invalid_png_size": "Dimensione immagine PNG non valida",
|
||||
"invalid_disclaimer_url": "L'URL del Disclaimer deve essere un link http o https"
|
||||
},
|
||||
"events": {
|
||||
"search": "Cerca eventi",
|
||||
"fs_events": "Eventi filesystem",
|
||||
|
|
|
@ -15,6 +15,36 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
-->
|
||||
{{template "base" .}}
|
||||
|
||||
{{- define "extra_css"}}
|
||||
<style {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
|
||||
.image-input-placeholder {
|
||||
background-image: url('{{.StaticURL}}{{.Branding.DefaultLogoPath}}');
|
||||
}
|
||||
|
||||
/*{{- if ne .Branding.DefaultLogoPath .Branding.LogoPath}}*/
|
||||
.image-input-webadmin-current {
|
||||
background-image: url('{{.StaticURL}}{{.Branding.LogoPath}}');
|
||||
}
|
||||
/*{{- end}}*/
|
||||
/*{{- if ne .Branding.DefaultFaviconPath .Branding.FaviconPath}}*/
|
||||
.image-input-webadmin-fav-current {
|
||||
background-image: url('{{.StaticURL}}{{.Branding.FaviconPath}}');
|
||||
}
|
||||
/*{{- end}}*/
|
||||
|
||||
/*{{- if ne .Branding.DefaultLogoPath .WebClientBranding.LogoPath}}*/
|
||||
.image-input-webclient-current {
|
||||
background-image: url('{{.StaticURL}}{{.WebClientBranding.LogoPath}}');
|
||||
}
|
||||
/*{{- end}}*/
|
||||
/*{{- if ne .Branding.DefaultFaviconPath .WebClientBranding.FaviconPath}}*/
|
||||
.image-input-webclient-fav-current {
|
||||
background-image: url('{{.StaticURL}}{{.WebClientBranding.FaviconPath}}');
|
||||
}
|
||||
/*{{- end}}*/
|
||||
</style>
|
||||
{{- end}}
|
||||
|
||||
{{- define "page_body"}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
|
@ -352,6 +382,221 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="accordion_header_branding">
|
||||
<button class="accordion-button section-title-inner text-primary collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion_branding_body" aria-expanded="{{if eq .ConfigSection 4}}true{{else}}false{{end}}" aria-controls="accordion_branding_body">
|
||||
<span data-i18n="branding.title">Branding</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="accordion_branding_body" class="accordion-collapse collapse {{if eq .ConfigSection 4}}show{{end}}" aria-labelledby="accordion_header_branding" data-bs-parent="#accordion_configs">
|
||||
<div class="accordion-body">
|
||||
{{- template "infomsg" "branding.help"}}
|
||||
<form id="configs_branding_form" enctype="multipart/form-data" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h3 class="card-title section-title-inner">WebAdmin</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label for="idBrandingWebAdminName" data-i18n="general.name" class="col-md-3 col-form-label">Name</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebAdminName" type="text" placeholder="SFTPGo WebAdmin" name="branding_webadmin_name" value="{{.Configs.Branding.WebAdmin.Name}}" maxlength="255"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebAdminShortName" data-i18n="branding.short_name" class="col-md-3 col-form-label">Short Name</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebAdminShortName" type="text" placeholder="WebAdmin" name="branding_webadmin_short_name" value="{{.Configs.Branding.WebAdmin.ShortName}}" maxlength="255"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebAdminLogo" data-i18n="branding.logo" class="col-md-3 col-form-label">Logo</label>
|
||||
<div class="col-md-9">
|
||||
<div class="image-input image-input-outline {{if eq .Branding.DefaultLogoPath .Branding.LogoPath}}image-input-empty{{end}} image-input-placeholder" data-kt-image-input="true">
|
||||
<div class="image-input-wrapper w-125px h-125px image-input-webadmin-current"></div>
|
||||
|
||||
<label class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="change">
|
||||
<i class="ki-duotone ki-pencil fs-6"><span class="path1"></span><span class="path2"></span></i>
|
||||
<input type="file" id="idBrandingWebAdminLogo" name="branding_webadmin_logo" accept=".png" aria-describedby="idBrandingWebAdminLogoHelp"/>
|
||||
<input type="hidden"name="branding_webadmin_logo_remove" />
|
||||
</label>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="cancel">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="remove">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div id="idBrandingWebAdminLogoHelp" class="form-text mt-3" data-i18n="branding.logo_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebAdminFavicon" data-i18n="branding.favicon" class="col-md-3 col-form-label">Favicon</label>
|
||||
<div class="col-md-9">
|
||||
<div class="image-input image-input-outline {{if eq .Branding.DefaultFaviconPath .Branding.FaviconPath}}image-input-empty{{end}} image-input-placeholder" data-kt-image-input="true">
|
||||
<div class="image-input-wrapper w-50px h-50px image-input-webadmin-fav-current"></div>
|
||||
|
||||
<label class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="change">
|
||||
<i class="ki-duotone ki-pencil fs-6"><span class="path1"></span><span class="path2"></span></i>
|
||||
<input type="file" id="idBrandingWebAdminFavicon" name="branding_webadmin_favicon" accept=".png" />
|
||||
<input type="hidden"name="branding_webadmin_favicon_remove" />
|
||||
</label>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="cancel">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="remove">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebAdminDisclaimerName" data-i18n="branding.disclaimer_name" class="col-md-3 col-form-label">Disclaimer Name</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebAdminDisclaimerName" type="text" placeholder="" name="branding_webadmin_disclaimer_name" value="{{.Configs.Branding.WebAdmin.DisclaimerName}}" maxlength="255"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebAdminDisclaimerURL" data-i18n="branding.disclaimer_url" class="col-md-3 col-form-label">Disclaimer URL</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebAdminDisclaimerURL" type="text" placeholder="" name="branding_webadmin_disclaimer_url" value="{{.Configs.Branding.WebAdmin.DisclaimerURL}}" maxlength="1024"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-10">
|
||||
<div class="card-header bg-light">
|
||||
<h3 class="card-title section-title-inner">WebClient</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row">
|
||||
<label for="idBrandingWebClientName" data-i18n="general.name" class="col-md-3 col-form-label">Name</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebClientName" type="text" placeholder="SFTPGo WebClient" name="branding_webclient_name" value="{{.Configs.Branding.WebClient.Name}}" maxlength="255"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebClientShortName" data-i18n="branding.short_name" class="col-md-3 col-form-label">Short Name</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebClientShortName" type="text" placeholder="WebClient" name="branding_webclient_short_name" value="{{.Configs.Branding.WebClient.ShortName}}" maxlength="255"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebClientLogo" data-i18n="branding.logo" class="col-md-3 col-form-label">Logo</label>
|
||||
<div class="col-md-9">
|
||||
<div class="image-input image-input-outline {{if eq .Branding.DefaultLogoPath .WebClientBranding.LogoPath}}image-input-empty{{end}} image-input-placeholder" data-kt-image-input="true">
|
||||
<div class="image-input-wrapper w-125px h-125px image-input-webclient-current"></div>
|
||||
|
||||
<label class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="change">
|
||||
<i class="ki-duotone ki-pencil fs-6"><span class="path1"></span><span class="path2"></span></i>
|
||||
<input type="file" id="idBrandingWebClientLogo" name="branding_webclient_logo" accept=".png" aria-describedby="idBrandingWebClientLogoHelp"/>
|
||||
<input type="hidden"name="branding_webclient_logo_remove" />
|
||||
</label>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="cancel">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="remove">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div id="idBrandingWebClientLogoHelp" class="form-text mt-3" data-i18n="branding.logo_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebClientFavicon" data-i18n="branding.favicon" class="col-md-3 col-form-label">Favicon</label>
|
||||
<div class="col-md-9">
|
||||
<div class="image-input image-input-outline {{if eq .Branding.DefaultFaviconPath .WebClientBranding.FaviconPath}}image-input-empty{{end}} image-input-placeholder" data-kt-image-input="true">
|
||||
<div class="image-input-wrapper w-50px h-50px image-input-webclient-fav-current"></div>
|
||||
|
||||
<label class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="change">
|
||||
<i class="ki-duotone ki-pencil fs-6"><span class="path1"></span><span class="path2"></span></i>
|
||||
<input type="file" id="idBrandingWebClientFavicon" name="branding_webclient_favicon" accept=".png" />
|
||||
<input type="hidden"name="branding_webclient_favicon_remove" />
|
||||
</label>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="cancel">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
|
||||
<span class="btn btn-icon btn-circle btn-color-muted btn-active-color-primary w-25px h-25px bg-body shadow"
|
||||
data-kt-image-input-action="remove">
|
||||
<i class="ki-outline ki-cross fs-3"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebClientDisclaimerName" data-i18n="branding.disclaimer_name" class="col-md-3 col-form-label">Disclaimer Name</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebClientDisclaimerName" type="text" placeholder="" name="branding_webclient_disclaimer_name" value="{{.Configs.Branding.WebClient.DisclaimerName}}" maxlength="255"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-10">
|
||||
<label for="idBrandingWebClientDisclaimerURL" data-i18n="branding.disclaimer_url" class="col-md-3 col-form-label">Disclaimer URL</label>
|
||||
<div class="col-md-9">
|
||||
<input id="idBrandingWebClientDisclaimerURL" type="text" placeholder="" name="branding_webclient_disclaimer_url" value="{{.Configs.Branding.WebClient.DisclaimerURL}}" maxlength="1024"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mt-12">
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<input type="hidden" name="form_action" value="branding_submit">
|
||||
<button type="submit" id="branding_form_submit" class="btn btn-primary px-10">
|
||||
<span data-i18n="general.submit" class="indicator-label">
|
||||
Submit
|
||||
</span>
|
||||
<span data-i18n="general.wait" class="indicator-progress">
|
||||
Please wait...
|
||||
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -591,6 +836,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
submitButton.setAttribute('data-kt-indicator', 'on');
|
||||
submitButton.disabled = true;
|
||||
});
|
||||
|
||||
$('#configs_branding_form').submit(function (event) {
|
||||
let submitButton = document.querySelector('#branding_form_submit');
|
||||
submitButton.setAttribute('data-kt-indicator', 'on');
|
||||
submitButton.disabled = true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{- end}}
|
Loading…
Reference in a new issue