WIP new WebAdmin: group page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-14 09:09:42 +01:00
parent 5c8214e121
commit bf94f8b87c
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
11 changed files with 815 additions and 1120 deletions

View file

@ -370,6 +370,8 @@ Thank you to [ysura](https://www.ysura.com/) for granting us stable access to a
Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [Mega Bundle](https://keenthemes.com/products/templates-mega-bundle) for SFTPGo UI.
Thank you to [Incode](https://www.incode.it/) for helping us to improve the UI/UX.
## License
GNU AGPL-3.0-only

15
go.mod
View file

@ -16,7 +16,7 @@ require (
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cockroachdb/cockroach-go/v2 v2.3.5
@ -36,7 +36,7 @@ require (
github.com/hashicorp/go-hclog v1.6.2
github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/jackc/pgx/v5 v5.5.1
github.com/jackc/pgx/v5 v5.5.2
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.17.4
github.com/lestrrat-go/jwx/v2 v2.0.19
@ -74,12 +74,12 @@ require (
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
golang.org/x/time v0.5.0
google.golang.org/api v0.155.0
google.golang.org/api v0.156.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
cloud.google.com/go v0.111.0 // indirect
cloud.google.com/go v0.112.0 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
@ -136,7 +136,6 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -145,7 +144,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/common v0.46.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@ -165,11 +164,11 @@ require (
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.org/x/tools v0.17.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect

34
go.sum
View file

@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
@ -65,8 +65,8 @@ github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6 h1:JWy+uLKZQR/9
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6/go.mod h1:T2NcfuIuXWcuwVwg3rBIW6h1cfzCdrzSn4Hs0KltND8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 h1:PJTdBMsyvra6FtED7JZtDpQrIAflYDHFoZAu/sKYkwU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.1 h1:Sn3MAV9YeACCULaxNWWYFH1a6G4wYFwBn3/TA5MwE2Q=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.1/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1vkSIWAN8tPwPCoYZBp7aruR540=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4=
@ -235,8 +235,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA=
github.com/jackc/pgx/v5 v5.5.2/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
@ -288,8 +288,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mhale/smtpd v0.8.1 h1:O02u8O3eYAGxZCGf4E98WjyB+rA3DVFZtchEialjX4s=
github.com/mhale/smtpd v0.8.1/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
@ -330,8 +328,8 @@ github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlk
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@ -419,8 +417,8 @@ go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
@ -430,8 +428,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -516,16 +514,16 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA=
google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk=
google.golang.org/api v0.156.0 h1:yloYcGbBtVYjLKQe4enCunxvwn3s2w/XPrrhVf6MsvQ=
google.golang.org/api v0.156.0/go.mod h1:bUSmn4KFO0Q+69zo9CNIDp4Psi6BqM0np0CbzKRSiSY=
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/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=

View file

@ -152,6 +152,14 @@ func getI18NErrorString(err error, fallback string) string {
return fallback
}
func getI18nError(err error) *util.I18nError {
var errI18n *util.I18nError
if err != nil {
errI18n = util.NewI18nError(err, util.I18nError500Message)
}
return errI18n
}
func handlePingRequest(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
render.PlainText(w, r, "PONG")

View file

@ -316,7 +316,7 @@ type folderPage struct {
type groupPage struct {
basePage
Group *dataprovider.Group
Error string
Error *util.I18nError
Mode genericPageMode
ValidPerms []string
ValidLoginMethods []string
@ -447,10 +447,9 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateGroups),
}
groupPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
filepath.Join(templatesPath, templateAdminDir, templateGroup),
}
eventRulesPaths := []string{
@ -993,14 +992,10 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
if errGroups != nil {
return
}
var errI18n *util.I18nError
if err != nil {
errI18n = util.NewI18nError(err, util.I18nError500Message)
}
data := userPage{
basePage: basePage,
Mode: mode,
Error: errI18n,
Error: getI18nError(err),
User: user,
ValidPerms: dataprovider.ValidPerms,
ValidLoginMethods: dataprovider.ValidLoginMethods,
@ -1067,10 +1062,10 @@ func (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, rol
}
func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
mode genericPageMode, error string,
mode genericPageMode, err error,
) {
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
if err != nil {
folders, errFolders := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
if errFolders != nil {
return
}
group.SetEmptySecretsIfNil()
@ -1078,10 +1073,10 @@ func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, gr
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add a new group"
title = util.I18nAddGroupTitle
currentURL = webGroupPath
case genericPageModeUpdate:
title = "Update group"
title = util.I18nUpdateGroupTitle
currentURL = fmt.Sprintf("%v/%v", webGroupPath, url.PathEscape(group.Name))
}
group.UserSettings.FsConfig.RedactedSecret = redactedSecret
@ -1089,7 +1084,7 @@ func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, gr
data := groupPage{
basePage: s.getBasePageData(title, currentURL, r),
Error: error,
Error: getI18nError(err),
Group: &group,
Mode: mode,
ValidPerms: dataprovider.ValidPerms,
@ -1977,7 +1972,7 @@ func getQuotaLimits(r *http.Request) (int64, int, error) {
return quotaSize, quotaFiles, nil
}
func updateUserFormFields(r *http.Request) {
func updateRepeaterFormFields(r *http.Request) {
for k := range r.Form {
if hasPrefixAndSuffix(k, "public_keys[", "][public_key]") {
r.Form.Add("public_keys", r.Form.Get(k))
@ -2029,7 +2024,9 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
return user, util.NewI18nError(err, util.I18nErrorInvalidForm)
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
updateUserFormFields(r)
updateRepeaterFormFields(r)
uid, err := strconv.Atoi(r.Form.Get("uid"))
if err != nil {
return user, fmt.Errorf("invalid uid: %w", err)
@ -2118,10 +2115,12 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
group := dataprovider.Group{}
err := r.ParseMultipartForm(maxRequestSize)
if err != nil {
return group, err
return group, util.NewI18nError(err, util.I18nErrorInvalidForm)
}
defer r.MultipartForm.RemoveAll() //nolint:errcheck
updateRepeaterFormFields(r)
maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
if err != nil {
return group, fmt.Errorf("invalid max sessions: %w", err)
@ -3536,7 +3535,7 @@ func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request)
func (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderGroupPage(w, r, dataprovider.Group{}, genericPageModeAdd, "")
s.renderGroupPage(w, r, dataprovider.Group{}, genericPageModeAdd, nil)
}
func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) {
@ -3548,7 +3547,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque
}
group, err := getGroupFromPostFields(r)
if err != nil {
s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
s.renderGroupPage(w, r, group, genericPageModeAdd, err)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3558,7 +3557,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque
}
err = dataprovider.AddGroup(&group, claims.Username, ipAddr, claims.Role)
if err != nil {
s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
s.renderGroupPage(w, r, group, genericPageModeAdd, err)
return
}
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
@ -3569,7 +3568,7 @@ func (s *httpdServer) handleWebUpdateGroupGet(w http.ResponseWriter, r *http.Req
name := getURLParam(r, "name")
group, err := dataprovider.GroupExists(name)
if err == nil {
s.renderGroupPage(w, r, group, genericPageModeUpdate, "")
s.renderGroupPage(w, r, group, genericPageModeUpdate, nil)
} else if errors.Is(err, util.ErrNotFound) {
s.renderNotFoundPage(w, r, err)
} else {
@ -3595,7 +3594,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
}
updatedGroup, err := getGroupFromPostFields(r)
if err != nil {
s.renderGroupPage(w, r, group, genericPageModeUpdate, err.Error())
s.renderGroupPage(w, r, group, genericPageModeUpdate, err)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3616,7 +3615,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr, claims.Role)
if err != nil {
s.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err.Error())
s.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err)
return
}
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)

View file

@ -180,6 +180,8 @@ const (
I18nErrorEndpointInvalid = "storage.endpoint_invalid"
I18nErrorEndpointRequired = "storage.endpoint_required"
I18nErrorFsUsernameRequired = "storage.username_required"
I18nAddGroupTitle = "title.add_group"
I18nUpdateGroupTitle = "title.update_group"
)
// NewI18nError returns a I18nError wrappring the provided error

View file

@ -47,7 +47,9 @@
"status": "Status",
"add_user": "Add user",
"update_user": "Update user",
"template_user": "User template"
"template_user": "User template",
"add_group": "Add group",
"update_group": "Update group"
},
"setup": {
"desc": "To start using SFTPGo you need to create an administrator user",
@ -462,7 +464,9 @@
"disconnect_help": "This way you force the user to login again, if connected, and so to use the new configuration",
"submit_generate": "Generate and save users",
"submit_export": "Generate and export users",
"invalid_quota_size": "Invalid quota size"
"invalid_quota_size": "Invalid quota size",
"expires_in": "Expires in",
"expires_in_help": "Account expiration as number of days from the creation. 0 means no expiration"
},
"group": {
"view_manage": "View and manage groups",
@ -621,7 +625,7 @@
"is_anonymous_help": "Anonymous users are supported for FTP and WebDAV protocols and have read-only access",
"disable_fs_checks": "Disable filesystem checks",
"disable_fs_checks_help": "Disable checks for existence and automatic creation of home directory and virtual folders",
"api_key_auth_help": "Allow to impersonate this user, in REST API, with an API key",
"api_key_auth_help": "Allow to impersonate the user, in REST API, with an API key",
"external_auth_cache_time": "External auth cache time",
"external_auth_cache_time_help": "Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache"
}

View file

@ -47,7 +47,9 @@
"status": "Stato",
"add_user": "Aggiungi utente",
"update_user": "Aggiorna utente",
"template_user": "Modello utente"
"template_user": "Modello utente",
"add_group": "Aggiungi gruppo",
"update_group": "Aggiorna gruppo"
},
"setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
@ -462,7 +464,9 @@
"disconnect_help": "In questo modo si obbliga l'utente a effettuare nuovamente il login, se connesso, e quindi ad utilizzare la nuova configurazione",
"submit_generate": "Genera e salva utenti",
"submit_export": "Genera ed esporta utenti",
"invalid_quota_size": "Quota (dimensione) non valida"
"invalid_quota_size": "Quota (dimensione) non valida",
"expires_in": "Scadenza",
"expires_in_help": "Scadenza dell'account espressa in numero di giorni dalla creazione. 0 significa nessuna scadenza"
},
"group": {
"view_manage": "Visualizza e gestisci gruppi",
@ -621,7 +625,7 @@
"is_anonymous_help": "Gli utenti anonimi sono supportati per i protocolli FTP e WebDAV e hanno accesso di sola lettura",
"disable_fs_checks": "Disabilita i controlli del filesystem",
"disable_fs_checks_help": "Disabilita i controlli sull'esistenza e la creazione automatica della directory home e delle cartelle virtuali",
"api_key_auth_help": "Permetti di impersonare questo utente nelle API REST utilizzando una chiave API",
"api_key_auth_help": "Permetti di impersonare l'utente nelle API REST utilizzando una chiave API",
"external_auth_cache_time": "Cache per autenticazione esterna",
"external_auth_cache_time_help": "Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache"
}

View file

@ -508,4 +508,358 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
{{- end}}
{{- define "user_group_perms"}}
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="filters.directory_patterns" class="card-title section-title-inner">Per-directory name patterns restrictions</h3>
</div>
<div class="card-body">
<div id="directory_patterns">
<p class="fs-5 fw-semibold mb-4" data-i18n="filters.directory_patterns_help"></p>
<div class="form-group">
<div data-repeater-list="directory_patterns">
{{- range $idx, $pattern := .GetFlatFilePatterns -}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]filters.directory_path_help" type="text" class="form-control" name="pattern_path" value="{{$pattern.Path}}" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input type="text" class="form-control" name="patterns" placeholder="*.png,*.zip" value="{{$pattern.GetCommaSeparatedPatterns}}" />
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_type" class="form-select select-repetear select-first" data-hide-search="true">
<option value="denied" data-i18n="general.denied" {{- if $pattern.IsDenied}} selected{{- end}}>Denied</option>
<option value="allowed" data-i18n="general.allowed" {{- if $pattern.IsAllowed}} selected{{- end}}>Allowed</option>
</select>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_policy" class="form-select select-repetear select-first" data-hide-search="true">
<option value="0" data-i18n="general.visible" {{- if eq $pattern.DenyPolicy 0}} selected{{- end}}>Visible</option>
<option value="1" data-i18n="general.hidden" {{- if eq $pattern.DenyPolicy 1}} selected{{- end}}>Hidden</option>
</select>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]filters.directory_path_help" type="text" class="form-control" name="pattern_path" value="" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input type="text" class="form-control" name="patterns" placeholder="*.png,*.zip" value="" />
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_type" class="form-select select-repetear select-first" data-hide-search="true">
<option value="denied" data-i18n="general.denied">Denied</option>
<option value="allowed" data-i18n="general.allowed">Allowed</option>
</select>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_policy" class="form-select select-repetear select-first" data-hide-search="true">
<option value="0" data-i18n="general.visible">Visible</option>
<option value="1" data-i18n="general.hidden">Hidden</option>
</select>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
{{- end}}
{{- define "user_group_quota"}}
<div class="form-group row mt-10">
<label for="idQuotaSize" data-i18n="virtual_folders.quota_size" class="col-md-3 col-form-label">Quota size</label>
<div class="col-md-3">
<input id="idQuotaSize" type="text" class="form-control" name="quota_size" value="{{HumanizeBytes .QuotaSize}}" aria-describedby="idQuotaSizeHelp" />
<div id="idQuotaSizeHelp" class="form-text" data-i18n="virtual_folders.quota_size_help"></div>
</div>
<div class="col-md-1"></div>
<label for="idQuotaFiles" data-i18n="virtual_folders.quota_files" class="col-md-2 col-form-label">Quota files</label>
<div class="col-md-3">
<input id="idQuotaFiles" type="number" min="0" class="form-control" name="quota_files" value="{{.QuotaFiles}}" aria-describedby="idQuotaFilesHelp" />
<div id="idQuotaFilesHelp" class="form-text" data-i18n="general.zero_no_limit_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idMaxUploadSize" data-i18n="filters.max_upload_size" class="col-md-3 col-form-label">Max file upload size</label>
<div class="col-md-9">
<input id="idMaxUploadSize" type="text" class="form-control" name="max_upload_file_size" value="{{HumanizeBytes .Filters.MaxUploadFileSize}}" aria-describedby="idMaxUploadSizeHelp" />
<div id="idMaxUploadSizeHelp" class="form-text" data-i18n="filters.max_upload_size_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idUploadBandwidth" data-i18n="filters.upload_bandwidth" class="col-md-3 col-form-label">Bandwidth UL (KB/s)</label>
<div class="col-md-3">
<input id="idUploadBandwidth" type="number" min="0" class="form-control" name="upload_bandwidth" value="{{.UploadBandwidth}}" aria-describedby="idUploadBandwidthHelp" />
<div id="idUploadBandwidthHelp" class="form-text" data-i18n="general.zero_no_limit_help"></div>
</div>
<div class="col-md-1"></div>
<label for="idDownloadBandwidth" data-i18n="filters.download_bandwidth" class="col-md-2 col-form-label">Bandwidth DL (KB/s)</label>
<div class="col-md-3">
<input id="idDownloadBandwidth" type="number" min="0" class="form-control" name="download_bandwidth" value="{{.DownloadBandwidth}}" aria-describedby="idDownloadBandwidthHelp" />
<div id="idDownloadBandwidthHelp" class="form-text" data-i18n="general.zero_no_limit_help"></div>
</div>
</div>
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="filters.src_bandwidth_limit" class="card-title section-title-inner">Per-source bandwidth speed limits</h3>
</div>
<div class="card-body">
<div id="src_bandwidth_limits">
<div class="form-group">
<div data-repeater-list="src_bandwidth_limits">
{{- range $idx, $bwLimit := .Filters.BandwidthLimits -}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-7 mt-3 mt-md-8">
<textarea class="form-control" name="bandwidth_limit_sources" rows="4">{{$bwLimit.GetSourcesAsString}}</textarea>
<div class="form-text" data-i18n="general.ip_mask_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="upload_bandwidth_source" value="{{$bwLimit.UploadBandwidth}}" />
<div class="form-text" data-i18n="filters.upload_bandwidth_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="download_bandwidth_source" value="{{$bwLimit.DownloadBandwidth}}" />
<div class="form-text" data-i18n="filters.download_bandwidth_help"></div>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-7 mt-3 mt-md-8">
<textarea class="form-control" name="bandwidth_limit_sources" rows="4"></textarea>
<div class="form-text" data-i18n="general.ip_mask_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="upload_bandwidth_source" value="" />
<div class="form-text" data-i18n="filters.upload_bandwidth_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="download_bandwidth_source" value="" />
<div class="form-text" data-i18n="filters.download_bandwidth_help"></div>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idTransferUL" data-i18n="filters.upload_data_transfer" class="col-md-3 col-form-label">Upload data transfer (MB)</label>
<div class="col-md-3">
<input id="idTransferUL" type="number" min="0" class="form-control" name="upload_data_transfer" value="{{.UploadDataTransfer}}" aria-describedby="idTransferULHelp" />
<div id="idTransferULHelp" class="form-text" data-i18n="filters.upload_data_transfer_help"></div>
</div>
<div class="col-md-1"></div>
<label for="idTransferDL" data-i18n="filters.download_data_transfer" class="col-md-2 col-form-label">Download data transfer (MB)</label>
<div class="col-md-3">
<input id="idTransferDL" type="number" min="0" class="form-control" name="download_data_transfer" value="{{.DownloadDataTransfer}}" aria-describedby="idTransferDLhHelp" />
<div id="idTransferDLhHelp" class="form-text" data-i18n="filters.download_data_transfer_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idTransferTotal" data-i18n="filters.total_data_transfer" class="col-md-3 col-form-label">Total data transfer (MB)</label>
<div class="col-md-9">
<input id="idTransferTotal" type="number" min="0" class="form-control" name="total_data_transfer" value="{{.TotalDataTransfer}}" aria-describedby="idTransferTotalHelp" />
<div id="idTransferTotalHelp" class="form-text" data-i18n="filters.total_data_transfer_help"></div>
</div>
</div>
{{- end}}
{{- define "user_group_advanced"}}
<div class="form-group row mt-10">
<label for="idStartDirectory" data-i18n="filters.start_directory" class="col-md-3 col-form-label">Start directory</label>
<div class="col-md-9">
<input id="idStartDirectory" type="text" class="form-control" name="start_directory" value="{{.StartDirectory}}" aria-describedby="idStartDirectoryHelp" />
<div id="idStartDirectoryHelp" class="form-text" data-i18n="filters.start_directory_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idTLSUsername" data-i18n="filters.tls_username" class="col-md-3 col-form-label">TLS username</label>
<div class="col-md-9">
<select id="idTLSUsername" name="tls_username" class="form-select" data-control="i18n-select2" data-hide-search="true" aria-describedby="idTLSUsernameHelp">
<option value="" {{if or (eq .TLSUsername "None") (eq .TLSUsername "") }}selected{{end}}>---</option>
<option value="CommonName" {{if eq .TLSUsername "CommonName" }}selected{{end}}>Common Name</option>
</select>
<div id="idTLSUsernameHelp" class="form-text" data-i18n="filters.tls_username_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idFTPSecurity" data-i18n="filters.ftp_security" class="col-md-3 col-form-label">FTP security</label>
<div class="col-md-9">
<select id="idFTPSecurity" name="ftp_security" class="form-select" data-control="i18n-select2" data-hide-search="true" aria-describedby="idFTPSecurityHelp">
<option value="" data-i18n="general.global_settings" {{if eq .FTPSecurity 0 }}selected{{end}}>Server settings</option>
<option value="1" data-i18n="general.mandatory_encryption" {{if eq .FTPSecurity 1 }}selected{{end}}>Mandatory encryption</option>
</select>
<div id="idFTPSecurityHelp" class="form-text" data-i18n="filters.ftp_security_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idHooks" data-i18n="filters.hooks" class="col-md-3 col-form-label">Hooks</label>
<div class="col-md-9">
<select id="idHooks" name="hooks" class="form-select" data-control="i18n-select2" data-hide-search="true" data-close-on-select="false" multiple>
<option value="external_auth_disabled" data-i18n="filters.hook_ext_auth_disabled" {{if .Hooks.ExternalAuthDisabled}}selected{{end}}>
External auth disabled
</option>
<option value="pre_login_disabled" data-i18n="filters.hook_pre_login_disabled" {{if .Hooks.PreLoginDisabled}}selected{{end}}>
Pre-login disabled
</option>
<option value="check_password_disabled" data-i18n="filters.hook_check_password_disabled" {{if .Hooks.CheckPasswordDisabled}}selected{{end}}>
Check password disabled
</option>
</select>
</div>
</div>
<div class="form-group row align-items-center mt-10">
<label data-i18n="filters.is_anonymous" class="col-md-3 col-form-label" for="idAnonymous">Is Anonymous</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idAnonymous" name="is_anonymous" {{if .IsAnonymous}}checked{{end}}/>
<label data-i18n="filters.is_anonymous_help" class="form-check-label fw-semibold text-gray-800" for="idAnonymous">
Anonymous users are supported for FTP and WebDAV protocols and have read-only access
</label>
</div>
</div>
</div>
<div class="form-group row align-items-center mt-10">
<label data-i18n="filters.disable_fs_checks" class="col-md-3 col-form-label" for="idDisableFsChecks">Disable filesystem checks</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idDisableFsChecks" name="disable_fs_checks" {{if .DisableFsChecks}}checked{{end}}/>
<label data-i18n="filters.disable_fs_checks_help" class="form-check-label fw-semibold text-gray-800" for="idDisableFsChecks">
Disable checks for existence and automatic creation of home directory and virtual folders
</label>
</div>
</div>
</div>
<div class="form-group row align-items-center mt-10">
<label data-i18n="general.api_key_auth" class="col-md-3 col-form-label" for="idAllowAPIKeyAuth">API key authentication</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if .AllowAPIKeyAuth}}checked{{end}}/>
<label data-i18n="filters.api_key_auth_help" class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth">
Allow to impersonate this user, in REST API, with an API key
</label>
</div>
</div>
</div>
{{- end}}
{{- define "user_group_profile"}}
<div class="form-group row mt-10">
<label for="idPasswordStrength" data-i18n="filters.password_strength" class="col-md-3 col-form-label">Password strength</label>
<div class="col-md-9">
<input id="idPasswordStrength" type="number" min="0" max="100" class="form-control" name="password_strength" value="{{.PasswordStrength}}" aria-describedby="idPasswordStrengthHelp"/>
<div id="idPasswordStrengthHelp" class="form-text" data-i18n="filters.password_strength_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idPasswordExpiration" data-i18n="filters.password_expiration" class="col-md-3 col-form-label">Password expiration</label>
<div class="col-md-9">
<input id="idPasswordExpiration" type="number" min="0" class="form-control" name="password_expiration" value="{{.PasswordExpiration}}" aria-describedby="idPasswordExpirationHelp"/>
<div id="idPasswordExpirationHelp" class="form-text" data-i18n="filters.password_expiration_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idDefaultSharesExpiration" data-i18n="filters.default_shares_expiration" class="col-md-3 col-form-label">Default shares expiration</label>
<div class="col-md-9">
<input id="idDefaultSharesExpiration" type="number" min="0" class="form-control" name="default_shares_expiration" value="{{.DefaultSharesExpiration}}" aria-describedby="idDefaultSharesExpirationHelp"/>
<div id="idDefaultSharesExpirationHelp" class="form-text" data-i18n="filters.default_shares_expiration_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idMaxSharesExpiration" data-i18n="filters.max_shares_expiration" class="col-md-3 col-form-label">Max shares expiration</label>
<div class="col-md-9">
<input id="idMaxSharesExpiration" type="number" min="0" class="form-control" name="max_shares_expiration" value="{{.MaxSharesExpiration}}" aria-describedby="idMaxSharesExpirationHelp"/>
<div id="idMaxSharesExpirationHelp" class="form-text" data-i18n="filters.max_shares_expiration_help"></div>
</div>
</div>
{{- end}}

File diff suppressed because it is too large Load diff

View file

@ -400,37 +400,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
<div class="form-group row mt-10">
<label for="idPasswordStrength" data-i18n="filters.password_strength" class="col-md-3 col-form-label">Password strength</label>
<div class="col-md-9">
<input id="idPasswordStrength" type="number" min="0" max="100" class="form-control" name="password_strength" value="{{.User.Filters.PasswordStrength}}" aria-describedby="idPasswordStrengthHelp"/>
<div id="idPasswordStrengthHelp" class="form-text" data-i18n="filters.password_strength_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idPasswordExpiration" data-i18n="filters.password_expiration" class="col-md-3 col-form-label">Password expiration</label>
<div class="col-md-9">
<input id="idPasswordExpiration" type="number" min="0" class="form-control" name="password_expiration" value="{{.User.Filters.PasswordExpiration}}" aria-describedby="idPasswordExpirationHelp"/>
<div id="idPasswordExpirationHelp" class="form-text" data-i18n="filters.password_expiration_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idDefaultSharesExpiration" data-i18n="filters.default_shares_expiration" class="col-md-3 col-form-label">Default shares expiration</label>
<div class="col-md-9">
<input id="idDefaultSharesExpiration" type="number" min="0" class="form-control" name="default_shares_expiration" value="{{.User.Filters.DefaultSharesExpiration}}" aria-describedby="idDefaultSharesExpirationHelp"/>
<div id="idDefaultSharesExpirationHelp" class="form-text" data-i18n="filters.default_shares_expiration_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idMaxSharesExpiration" data-i18n="filters.max_shares_expiration" class="col-md-3 col-form-label">Max shares expiration</label>
<div class="col-md-9">
<input id="idMaxSharesExpiration" type="number" min="0" class="form-control" name="max_shares_expiration" value="{{.User.Filters.MaxSharesExpiration}}" aria-describedby="idMaxSharesExpirationHelp"/>
<div id="idMaxSharesExpirationHelp" class="form-text" data-i18n="filters.max_shares_expiration_help"></div>
</div>
</div>
{{- template "user_group_profile" .User.Filters}}
<div class="form-group row mt-10">
<label for="idDescription" data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
@ -549,98 +519,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="filters.directory_patterns" class="card-title section-title-inner">Per-directory name patterns restrictions</h3>
</div>
<div class="card-body">
<div id="directory_patterns">
<p class="fs-5 fw-semibold mb-4" data-i18n="filters.directory_patterns_help"></p>
<div class="form-group">
<div data-repeater-list="directory_patterns">
{{- range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]filters.directory_path_help" type="text" class="form-control" name="pattern_path" value="{{$pattern.Path}}" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input type="text" class="form-control" name="patterns" placeholder="*.png,*.zip" value="{{$pattern.GetCommaSeparatedPatterns}}" />
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_type" class="form-select select-repetear select-first" data-hide-search="true">
<option value="denied" data-i18n="general.denied" {{- if $pattern.IsDenied}} selected{{- end}}>Denied</option>
<option value="allowed" data-i18n="general.allowed" {{- if $pattern.IsAllowed}} selected{{- end}}>Allowed</option>
</select>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_policy" class="form-select select-repetear select-first" data-hide-search="true">
<option value="0" data-i18n="general.visible" {{- if eq $pattern.DenyPolicy 0}} selected{{- end}}>Visible</option>
<option value="1" data-i18n="general.hidden" {{- if eq $pattern.DenyPolicy 1}} selected{{- end}}>Hidden</option>
</select>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-4 mt-3 mt-md-8">
<input data-i18n="[placeholder]filters.directory_path_help" type="text" class="form-control" name="pattern_path" value="" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input type="text" class="form-control" name="patterns" placeholder="*.png,*.zip" value="" />
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_type" class="form-select select-repetear select-first" data-hide-search="true">
<option value="denied" data-i18n="general.denied">Denied</option>
<option value="allowed" data-i18n="general.allowed">Allowed</option>
</select>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<select name="pattern_policy" class="form-select select-repetear select-first" data-hide-search="true">
<option value="0" data-i18n="general.visible">Visible</option>
<option value="1" data-i18n="general.hidden">Hidden</option>
</select>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
{{- template "user_group_perms" .User.Filters}}
<div class="form-group row mt-10">
<label for="idMaxSessions" data-i18n="filters.max_sessions" class="col-md-3 col-form-label">Max sessions</label>
@ -665,7 +544,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="form-group row mt-10">
<label for="idLoginMethods" data-i18n="filters.denied_login_methods" class="col-md-3 col-form-label">
Denied protocols
Denied login methods
</label>
<div class="col-md-9">
<select id="idLoginMethods" name="denied_login_methods" class="form-select" data-control="i18n-select2" data-close-on-select="false" multiple aria-describedby="idLoginMethodsHelp">
@ -717,7 +596,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<label for="idAllowedIP" data-i18n="general.allowed_ip_mask" class="col-md-3 col-form-label">Allowed IP/Mask</label>
<div class="col-md-9">
<textarea class="form-control" id="idAllowedIP" name="allowed_ip" aria-describedby="idAllowedIPHelp"
rows="3">{{.User.GetDeniedIPAsString}}</textarea>
rows="3">{{.User.GetAllowedIPAsString}}</textarea>
<div id="idAllowedIPHelp" class="form-text" data-i18n="general.ip_mask_help"></div>
</div>
</div>
@ -735,143 +614,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div id="collapseQuota" class="accordion-collapse collapse" aria-labelledby="headingQuota" data-bs-parent="#accordionUser">
<div class="accordion-body">
<div class="form-group row mt-10">
<label for="idQuotaSize" data-i18n="virtual_folders.quota_size" class="col-md-3 col-form-label">Quota size</label>
<div class="col-md-3">
<input id="idQuotaSize" type="text" class="form-control" name="quota_size" value="{{HumanizeBytes .User.QuotaSize}}" aria-describedby="idQuotaSizeHelp" />
<div id="idQuotaSizeHelp" class="form-text" data-i18n="virtual_folders.quota_size_help"></div>
</div>
<div class="col-md-1"></div>
<label for="idQuotaFiles" data-i18n="virtual_folders.quota_files" class="col-md-2 col-form-label">Quota files</label>
<div class="col-md-3">
<input id="idQuotaFiles" type="number" min="0" class="form-control" name="quota_files" value="{{.User.QuotaFiles}}" aria-describedby="idQuotaFilesHelp" />
<div id="idQuotaFilesHelp" class="form-text" data-i18n="general.zero_no_limit_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idMaxUploadSize" data-i18n="filters.max_upload_size" class="col-md-3 col-form-label">Max file upload size</label>
<div class="col-md-9">
<input id="idMaxUploadSize" type="text" class="form-control" name="max_upload_file_size" value="{{HumanizeBytes .User.Filters.MaxUploadFileSize}}" aria-describedby="idMaxUploadSizeHelp" />
<div id="idMaxUploadSizeHelp" class="form-text" data-i18n="filters.max_upload_size_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idUploadBandwidth" data-i18n="filters.upload_bandwidth" class="col-md-3 col-form-label">Bandwidth UL (KB/s)</label>
<div class="col-md-3">
<input id="idUploadBandwidth" type="number" min="0" class="form-control" name="upload_bandwidth" value="{{.User.UploadBandwidth}}" aria-describedby="idUploadBandwidthHelp" />
<div id="idUploadBandwidthHelp" class="form-text" data-i18n="general.zero_no_limit_help"></div>
</div>
<div class="col-md-1"></div>
<label for="idDownloadBandwidth" data-i18n="filters.download_bandwidth" class="col-md-2 col-form-label">Bandwidth DL (KB/s)</label>
<div class="col-md-3">
<input id="idDownloadBandwidth" type="number" min="0" class="form-control" name="download_bandwidth" value="{{.User.DownloadBandwidth}}" aria-describedby="idDownloadBandwidthHelp" />
<div id="idDownloadBandwidthHelp" class="form-text" data-i18n="general.zero_no_limit_help"></div>
</div>
</div>
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="filters.src_bandwidth_limit" class="card-title section-title-inner">Per-source bandwidth speed limits</h3>
</div>
<div class="card-body">
<div id="src_bandwidth_limits">
<div class="form-group">
<div data-repeater-list="src_bandwidth_limits">
{{- range $idx, $bwLimit := .User.Filters.BandwidthLimits -}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-7 mt-3 mt-md-8">
<textarea class="form-control" name="bandwidth_limit_sources" rows="4">{{$bwLimit.GetSourcesAsString}}</textarea>
<div class="form-text" data-i18n="general.ip_mask_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="upload_bandwidth_source" value="{{$bwLimit.UploadBandwidth}}" />
<div class="form-text" data-i18n="filters.upload_bandwidth_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="download_bandwidth_source" value="{{$bwLimit.DownloadBandwidth}}" />
<div class="form-text" data-i18n="filters.download_bandwidth_help"></div>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-7 mt-3 mt-md-8">
<textarea class="form-control" name="bandwidth_limit_sources" rows="4"></textarea>
<div class="form-text" data-i18n="general.ip_mask_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="upload_bandwidth_source" value="" />
<div class="form-text" data-i18n="filters.upload_bandwidth_help"></div>
</div>
<div class="col-md-2 mt-3 mt-md-8">
<input type="number" min="0" class="form-control" name="download_bandwidth_source" value="" />
<div class="form-text" data-i18n="filters.download_bandwidth_help"></div>
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idTransferUL" data-i18n="filters.upload_data_transfer" class="col-md-3 col-form-label">Upload data transfer (MB)</label>
<div class="col-md-3">
<input id="idTransferUL" type="number" min="0" class="form-control" name="upload_data_transfer" value="{{.User.UploadDataTransfer}}" aria-describedby="idTransferULHelp" />
<div id="idTransferULHelp" class="form-text" data-i18n="filters.upload_data_transfer_help"></div>
</div>
<div class="col-md-1"></div>
<label for="idTransferDL" data-i18n="filters.download_data_transfer" class="col-md-2 col-form-label">Download data transfer (MB)</label>
<div class="col-md-3">
<input id="idTransferDL" type="number" min="0" class="form-control" name="download_data_transfer" value="{{.User.DownloadDataTransfer}}" aria-describedby="idTransferDLhHelp" />
<div id="idTransferDLhHelp" class="form-text" data-i18n="filters.download_data_transfer_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idTransferTotal" data-i18n="filters.total_data_transfer" class="col-md-3 col-form-label">Total data transfer (MB)</label>
<div class="col-md-9">
<input id="idTransferTotal" type="number" min="0" class="form-control" name="total_data_transfer" value="{{.User.TotalDataTransfer}}" aria-describedby="idTransferTotalHelp" />
<div id="idTransferTotalHelp" class="form-text" data-i18n="filters.total_data_transfer_help"></div>
</div>
</div>
{{template "user_group_quota" .User}}
</div>
</div>
@ -886,50 +629,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div id="collapseAdvanced" class="accordion-collapse collapse" aria-labelledby="headingAdvanced" data-bs-parent="#accordionUser">
<div class="accordion-body">
<div class="form-group row mt-10">
<label for="idStartDirectory" data-i18n="filters.start_directory" class="col-md-3 col-form-label">Start directory</label>
<div class="col-md-9">
<input id="idStartDirectory" type="text" class="form-control" name="start_directory" value="{{.User.Filters.StartDirectory}}" aria-describedby="idStartDirectoryHelp" />
<div id="idStartDirectoryHelp" class="form-text" data-i18n="filters.start_directory_help"></div>
</div>
</div>
{{template "user_group_advanced" .User.Filters}}
<div class="form-group row mt-10">
<label for="idTLSUsername" data-i18n="filters.tls_username" class="col-md-3 col-form-label">TLS username</label>
<div class="form-group row mt-10 {{if not .User.HasExternalAuth}}d-none{{end}}">
<label for="idExtAuthCacheTime" data-i18n="filters.external_auth_cache_time" class="col-md-3 col-form-label">External auth cache time</label>
<div class="col-md-9">
<select id="idTLSUsername" name="tls_username" class="form-select" data-control="i18n-select2" data-hide-search="true" aria-describedby="idTLSUsernameHelp">
<option value="" {{if or (eq .User.Filters.TLSUsername "None") (eq .User.Filters.TLSUsername "") }}selected{{end}}>---</option>
<option value="CommonName" {{if eq .User.Filters.TLSUsername "CommonName" }}selected{{end}}>Common Name</option>
</select>
<div id="idTLSUsernameHelp" class="form-text" data-i18n="filters.tls_username_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idFTPSecurity" data-i18n="filters.ftp_security" class="col-md-3 col-form-label">FTP security</label>
<div class="col-md-9">
<select id="idFTPSecurity" name="ftp_security" class="form-select" data-control="i18n-select2" data-hide-search="true" aria-describedby="idFTPSecurityHelp">
<option value="" data-i18n="general.global_settings" {{if eq .User.Filters.FTPSecurity 0 }}selected{{end}}>Server settings</option>
<option value="1" data-i18n="general.mandatory_encryption" {{if eq .User.Filters.FTPSecurity 1 }}selected{{end}}>Mandatory encryption</option>
</select>
<div id="idFTPSecurityHelp" class="form-text" data-i18n="filters.ftp_security_help"></div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idHooks" data-i18n="filters.hooks" class="col-md-3 col-form-label">Hooks</label>
<div class="col-md-9">
<select id="idHooks" name="hooks" class="form-select" data-control="i18n-select2" data-hide-search="true" data-close-on-select="false" multiple>
<option value="external_auth_disabled" data-i18n="filters.hook_ext_auth_disabled" {{if .User.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
External auth disabled
</option>
<option value="pre_login_disabled" data-i18n="filters.hook_pre_login_disabled" {{if .User.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
Pre-login disabled
</option>
<option value="check_password_disabled" data-i18n="filters.hook_check_password_disabled" {{if .User.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
Check password disabled
</option>
</select>
<input id="idExtAuthCacheTime" type="number" min="0" class="form-control" name="external_auth_cache_time" value="{{.User.Filters.ExternalAuthCacheTime}}" aria-describedby="idExtAuthCacheTimeHelp" />
<div id="idExtAuthCacheTimeHelp" class="form-text" data-i18n="filters.external_auth_cache_time_help"></div>
</div>
</div>
@ -945,50 +651,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
<div class="form-group row align-items-center mt-10">
<label data-i18n="filters.is_anonymous" class="col-md-3 col-form-label" for="idAnonymous">Is Anonymous</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idAnonymous" name="is_anonymous" {{if .User.Filters.IsAnonymous}}checked{{end}}/>
<label data-i18n="filters.is_anonymous_help" class="form-check-label fw-semibold text-gray-800" for="idAnonymous">
Anonymous users are supported for FTP and WebDAV protocols and have read-only access
</label>
</div>
</div>
</div>
<div class="form-group row align-items-center mt-10">
<label data-i18n="filters.disable_fs_checks" class="col-md-3 col-form-label" for="idDisableFsChecks">Disable filesystem checks</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idDisableFsChecks" name="disable_fs_checks" {{if .User.Filters.DisableFsChecks}}checked{{end}}/>
<label data-i18n="filters.disable_fs_checks_help" class="form-check-label fw-semibold text-gray-800" for="idDisableFsChecks">
Disable checks for existence and automatic creation of home directory and virtual folders
</label>
</div>
</div>
</div>
<div class="form-group row align-items-center mt-10">
<label data-i18n="general.api_key_auth" class="col-md-3 col-form-label" for="idAllowAPIKeyAuth">API key authentication</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if .User.Filters.AllowAPIKeyAuth}}checked{{end}}/>
<label data-i18n="filters.api_key_auth_help" class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth">
Allow to impersonate this user, in REST API, with an API key
</label>
</div>
</div>
</div>
<div class="form-group row mt-10 {{if not .User.HasExternalAuth}}d-none{{end}}">
<label for="idExtAuthCacheTime" data-i18n="filters.external_auth_cache_time" class="col-md-3 col-form-label">External auth cache time</label>
<div class="col-md-9">
<input id="idExtAuthCacheTime" type="number" min="0" class="form-control" name="external_auth_cache_time" value="{{.User.Filters.ExternalAuthCacheTime}}" aria-describedby="idExtAuthCacheTimeHelp" />
<div id="idExtAuthCacheTimeHelp" class="form-text" data-i18n="filters.external_auth_cache_time_help"></div>
</div>
</div>
</div>
</div>
</div>