WebClient WIP: add support for localizations

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-12-10 16:40:13 +01:00
parent 7572daf9cc
commit c71f0426ae
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
54 changed files with 6160 additions and 1100 deletions

View file

@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: '1.21'

View file

@ -25,7 +25,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
@ -230,7 +230,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: '1.21'
@ -294,7 +294,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: '1.21'
@ -515,7 +515,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: '1.21'
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -14,7 +14,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
@ -48,7 +48,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}

81
go.mod
View file

@ -9,37 +9,37 @@ require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v1.0.0 github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.23.1 github.com/aws/aws-sdk-go-v2 v1.24.0
github.com/aws/aws-sdk-go-v2/config v1.25.6 github.com/aws/aws-sdk-go-v2/config v1.26.1
github.com/aws/aws-sdk-go-v2/credentials v1.16.5 github.com/aws/aws-sdk-go-v2/credentials v1.16.12
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.4 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.4 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.45.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.24.1 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5
github.com/aws/aws-sdk-go-v2/service/sts v1.25.5 github.com/aws/aws-sdk-go-v2/service/sts v1.26.5
github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cockroachdb/cockroach-go/v2 v2.3.5 github.com/cockroachdb/cockroach-go/v2 v2.3.5
github.com/coreos/go-oidc/v3 v3.8.0 github.com/coreos/go-oidc/v3 v3.9.0
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.22.0 github.com/fclairamb/ftpserverlib v0.22.0
github.com/fclairamb/go-log v0.4.1 github.com/fclairamb/go-log v0.4.1
github.com/go-acme/lego/v4 v4.14.2 github.com/go-acme/lego/v4 v4.14.2
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/jwtauth/v5 v5.2.0 github.com/go-chi/jwtauth/v5 v5.3.0
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.4.0 github.com/google/uuid v1.4.0
github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-hclog v1.6.1
github.com/hashicorp/go-plugin v1.6.0 github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.5 github.com/hashicorp/go-retryablehttp v0.7.5
github.com/jackc/pgx/v5 v5.5.0 github.com/jackc/pgx/v5 v5.5.1
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.17.3 github.com/klauspost/compress v1.17.4
github.com/lestrrat-go/jwx/v2 v2.0.17 github.com/lestrrat-go/jwx/v2 v2.0.18
github.com/lithammer/shortuuid/v3 v3.0.7 github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.18 github.com/mattn/go-sqlite3 v1.14.18
github.com/mhale/smtpd v0.8.0 github.com/mhale/smtpd v0.8.0
@ -54,10 +54,10 @@ require (
github.com/rs/xid v1.5.0 github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25 github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25
github.com/shirou/gopsutil/v3 v3.23.10 github.com/shirou/gopsutil/v3 v3.23.11
github.com/spf13/afero v1.11.0 github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.17.0 github.com/spf13/viper v1.18.1
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/studio-b12/gowebdav v0.9.0 github.com/studio-b12/gowebdav v0.9.0
github.com/subosito/gotenv v1.6.0 github.com/subosito/gotenv v1.6.0
@ -67,36 +67,36 @@ require (
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.8 go.etcd.io/bbolt v1.3.8
go.uber.org/automaxprocs v1.5.3 go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.34.0 gocloud.dev v0.35.0
golang.org/x/crypto v0.16.0 golang.org/x/crypto v0.16.0
golang.org/x/net v0.19.0 golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.15.0 golang.org/x/oauth2 v0.15.0
golang.org/x/sys v0.15.0 golang.org/x/sys v0.15.0
golang.org/x/term v0.15.0 golang.org/x/term v0.15.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
google.golang.org/api v0.152.0 google.golang.org/api v0.153.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
cloud.google.com/go v0.110.10 // indirect cloud.google.com/go v0.111.0 // indirect
cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.5 // indirect cloud.google.com/go/iam v1.1.5 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect
github.com/aws/smithy-go v1.17.0 // indirect github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
@ -108,6 +108,8 @@ require (
github.com/fatih/color v1.16.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@ -120,7 +122,7 @@ require (
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
@ -145,19 +147,22 @@ require (
github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.7.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
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 go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.5.0 // indirect golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect

176
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.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= 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 v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
@ -14,16 +14,16 @@ cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYE
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EXeRrLJIwyGnJcAlAWKwhs= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+3LoSsYf9YMjkupeAnHMX8O9mmY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+3LoSsYf9YMjkupeAnHMX8O9mmY=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
@ -33,48 +33,48 @@ github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHc
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4= github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k= github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI= github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk=
github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
github.com/aws/aws-sdk-go-v2/config v1.25.6 h1:p7b0sR6lHVNNOK/dE4xZgq2R+NNFRjtAXy8WNE6jbpo= github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o=
github.com/aws/aws-sdk-go-v2/config v1.25.6/go.mod h1:E/nt0ERX9ZX2RCcJWBax94jFn738UERvjSn4R3msEeQ= github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg=
github.com/aws/aws-sdk-go-v2/credentials v1.16.5 h1:oJz7X2VzKl8Y9pX7Fa5sIy4+3OnknF+Ne0KYu7DCoQQ= github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU=
github.com/aws/aws-sdk-go-v2/credentials v1.16.5/go.mod h1:2HvVzcP9ih6XR66omXIsgWjtolkL0MlQVqPcK3nXK+E= github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 h1:KehRNiVzIfAcj6gw98zotVbb/K67taJE0fkfgM6vzqU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5/go.mod h1:VhnExhw6uXy9QzetvpXDolo1/hjhx4u9qukBGkuUwjs= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.4 h1:021nsPdnrpRJJHF4fAJb/uOTYeC1UHaWEtjAp7QLQmc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 h1:FnLf60PtjXp8ZOzQfhJVsqF0OtYKQZWQfqOLshh8YXg=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.4/go.mod h1:tb4mNW+u7WObIYuOj5rqjo5rZTkSQI677lhuPl1SLtA= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7/go.mod h1:tDVvl8hyU6E9B8TrnNrZQEVkQlB8hjJwcgpPhgtlnNg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 h1:LAm3Ycm9HJfbSCd5I+wqC2S9Ej7FPrgr5CQoOljJZcE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4/go.mod h1:xEhvbJcyUf/31yfGSQBe01fukXwXJ0gxDp7rLfymWE0= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 h1:4GV0kKZzUxiWxSVpn/9gwR0g21NF1Jsyduzo9rHgC/Q= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4/go.mod h1:dYvTNAggxDZy6y1AF7YDwXsPuHFy/VNEpEI/2dWK9IU= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 h1:40Q4X5ebZruRtknEZH/bg91sT5pR853F7/1X9QRbI54= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4/go.mod h1:u77N7eEECzUv7F0xl2gcfK/vzc8wcjWobpy+DcrLJ5E= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 h1:6DRKQc+9cChgzL5gplRGusI5dBGeiEod4m/pmGbcX48= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4/go.mod h1:s8ORvrW4g4v7IvYKIAoBg17w3GQ+XuwXDXYrQ5SkzU0= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 h1:rdovz3rEu0vZKbzoMYPTehp0E8veoE9AyfzqCr5Eeao= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 h1:o3DcfCxGDIT20pTbVKVhp3vWXOj/VvgazNJvumWeYW0= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4/go.mod h1:Uy0KVOxuTK2ne+/PKQ+VvEeWmjMMksE17k/2RK/r5oM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.4 h1:Du/aKtsWdiWsfUhnLsWOxG9gvO1SWbP7pw/kH6SBd78= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5 h1:Fp3Gcbp3lAJAxeFRVSxc6tWOUPSG8iSkJEiFl3eZZ3o=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.4/go.mod h1:2sF7pT0z9zalHaTQ2JeksaK3lOwj8Cu/znj0jONV/Jc= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5/go.mod h1:XABJbVXMa0xnVqaGbhkfUeVV0GrPsc3Jqscu87IovXU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.45.1 h1:D/QGsEd+pZNLFMA0PCU/aoYCRUWrMGtEwW5xy6OraSE= github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.45.1/go.mod h1:dqJ5JBL0clzgHriH35Amx3LRFY6wNIPUX7QO/BerSBo= github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.24.1 h1:zKRDPVW3scBeTl1/ZiNyLwpTxh4gZ48xQVIyaghb6+c= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5 h1:qYi/BfDrWXZxlmRjlKCyFmtI4HKJwW8OKDKhKRAOZQI=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.24.1/go.mod h1:LDD9wCQ1tvjMIWEIFPvZ8JgJsEOjded+X5jav9tD/zg= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5/go.mod h1:4Ae1NCLK6ghmjzd45Tc33GgCKhUWD2ORAlULtMO1Cbs=
github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 h1:WSMiDIMaDGyIiXwruNITU0IJF0d0foXwjxpxRylamqQ= github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM=
github.com/aws/aws-sdk-go-v2/service/sso v1.17.4/go.mod h1:oA6VjNsLll2eVuUoF2D+CMyORgNzPEW/3PyUdq6WQjI= github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 h1:GsrlsvTPBNxHvE3KBCwUMnR76MTO/6qnnO1ILSUOpTA= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2/go.mod h1:hHL974p5auvXlZPIjJTblXJpbkfK4klBczlsEaMCGVY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38=
github.com/aws/aws-sdk-go-v2/service/sts v1.25.5 h1:jwpmP8FnZPdpmJ8hkximoPQFGCUzfIekccwkxlfVfHQ= github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg=
github.com/aws/aws-sdk-go-v2/service/sts v1.25.5/go.mod h1:feTnm2Tk/pJxdX+eooEsxvlvTWBvDm6CasRZ+JOs2IY= github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU=
github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
@ -93,8 +93,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0= github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0=
github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8=
github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
@ -137,8 +137,8 @@ github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHG
github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c= github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.2.0 h1:rw2wRNY6QHxyjYhoZYrQ4IeXVpPeun9nCZ9DBItDFPc= github.com/go-chi/jwtauth/v5 v5.3.0 h1:X7RKGks1lrVeIe2omGyz47pNaNjG2YmwlRN5UKhN8qg=
github.com/go-chi/jwtauth/v5 v5.2.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY= github.com/go-chi/jwtauth/v5 v5.3.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
@ -147,6 +147,11 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -157,8 +162,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@ -209,8 +214,8 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.6.1 h1:pa92nu9bPoAqI7p+uPDCIWGAibUdlCi6TYWJEQQkLf8=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.6.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
@ -223,10 +228,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 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/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
@ -235,8 +240,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -253,8 +258,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.17 h1:+WavkdKVWO90ECnIzUetOnjY+kcqqw4WXEUmil7sMCE= github.com/lestrrat-go/jwx/v2 v2.0.18 h1:HHZkYS5wWDDyAiNBwztEtDoX07WDhGEdixm8G06R50o=
github.com/lestrrat-go/jwx/v2 v2.0.17/go.mod h1:G8randPHLGAqhcNCqtt6/V/7E6fvJRl3Sf9z777eTQ0= github.com/lestrrat-go/jwx/v2 v2.0.18/go.mod h1:fAJ+k5eTgKdDqanzCuK6DAt3W7n3cs2/FX7JhQdk83U=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
@ -335,8 +340,8 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
@ -345,8 +350,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25 h1:R8cTb41ZX5WSYw8q8ufTKQfOvXh7aLQWqdnteDY/96U= github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25 h1:R8cTb41ZX5WSYw8q8ufTKQfOvXh7aLQWqdnteDY/96U=
github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25/go.mod h1:6s/PFoLUd7FXG3wGlrdVhrA0SJOwri2h9kzTph/2oiU= github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25/go.mod h1:6s/PFoLUd7FXG3wGlrdVhrA0SJOwri2h9kzTph/2oiU=
github.com/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM= github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ=
github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE= github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@ -361,8 +366,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -380,10 +385,12 @@ github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk= github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
@ -400,15 +407,23 @@ go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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/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= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
gocloud.dev v0.34.0 h1:LzlQY+4l2cMtuNfwT2ht4+fiXwWf/NmPTnXUlLmGif4= gocloud.dev v0.35.0 h1:x/Gtt5OJdT4j+ir1AXAIXb7bBnFawXAAaJptCUGk3HU=
gocloud.dev v0.34.0/go.mod h1:psKOachbnvY3DAOPbsFVmLIErwsbWPUG2H5i65D38vE= gocloud.dev v0.35.0/go.mod h1:wbyF+BhfdtLWyUtVEWRW13hFLb1vXnV2ovEhYGQe3ck=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -465,7 +480,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -501,8 +515,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.152.0 h1:t0r1vPnfMc260S2Ci+en7kfCZaLOPs5KI0sVV/6jZrY= google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4=
google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=

View file

@ -1722,7 +1722,7 @@ func (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, strin
if c.protocol == ProtocolWebDAV && strings.Contains(err.Error(), vfs.ErrSFTPLoop.Error()) { if c.protocol == ProtocolWebDAV && strings.Contains(err.Error(), vfs.ErrSFTPLoop.Error()) {
// if there is an SFTP loop we return a permission error, for WebDAV, so the problematic folder // if there is an SFTP loop we return a permission error, for WebDAV, so the problematic folder
// will not be listed // will not be listed
return nil, "", c.GetPermissionDeniedError() return nil, "", util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)
} }
return nil, "", c.GetGenericError(err) return nil, "", c.GetGenericError(err)
} }

View file

@ -2632,14 +2632,23 @@ func buildUserHomeDir(user *User) {
func validateFolderQuotaLimits(folder vfs.VirtualFolder) error { func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
if folder.QuotaSize < -1 { if folder.QuotaSize < -1 {
return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %q", folder.QuotaSize, folder.MappedPath)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %q", folder.QuotaSize, folder.MappedPath)),
util.I18nErrorFolderQuotaSizeInvalid,
)
} }
if folder.QuotaFiles < -1 { if folder.QuotaFiles < -1 {
return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %q", folder.QuotaFiles, folder.MappedPath)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %q", folder.QuotaFiles, folder.MappedPath)),
util.I18nErrorFolderQuotaFileInvalid,
)
} }
if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) { if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) {
return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", return util.NewI18nError(
folder.QuotaFiles, folder.QuotaSize)) util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v",
folder.QuotaFiles, folder.QuotaSize)),
util.I18nErrorFolderQuotaInvalid,
)
} }
return nil return nil
} }
@ -2657,12 +2666,18 @@ func validateUserGroups(user *User) error {
} }
if g.Type == sdk.GroupTypePrimary { if g.Type == sdk.GroupTypePrimary {
if hasPrimary { if hasPrimary {
return util.NewValidationError("only one primary group is allowed") return util.NewI18nError(
util.NewValidationError("only one primary group is allowed"),
util.I18nErrorPrimaryGroup,
)
} }
hasPrimary = true hasPrimary = true
} }
if groupNames[g.Name] { if groupNames[g.Name] {
return util.NewValidationError(fmt.Sprintf("the group %q is duplicated", g.Name)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("the group %q is duplicated", g.Name)),
util.I18nErrorDuplicateGroup,
)
} }
groupNames[g.Name] = true groupNames[g.Name] = true
} }
@ -2678,22 +2693,31 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
for _, v := range vfolders { for _, v := range vfolders {
if v.VirtualPath == "" { if v.VirtualPath == "" {
return nil, util.NewValidationError("mount/virtual path is mandatory") return nil, util.NewI18nError(
util.NewValidationError("mount/virtual path is mandatory"),
util.I18nErrorFolderMountPathRequired,
)
} }
cleanedVPath := util.CleanPath(v.VirtualPath) cleanedVPath := util.CleanPath(v.VirtualPath)
if err := validateFolderQuotaLimits(v); err != nil { if err := validateFolderQuotaLimits(v); err != nil {
return nil, err return nil, err
} }
if v.Name == "" { if v.Name == "" {
return nil, util.NewValidationError("folder name is mandatory") return nil, util.NewI18nError(util.NewValidationError("folder name is mandatory"), util.I18nErrorFolderNameRequired)
} }
if folderNames[v.Name] { if folderNames[v.Name] {
return nil, util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name)) return nil, util.NewI18nError(
util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name)),
util.I18nErrorDuplicatedFolders,
)
} }
for _, vFolder := range virtualFolders { for _, vFolder := range virtualFolders {
if util.IsDirOverlapped(vFolder.VirtualPath, cleanedVPath, false, "/") { if util.IsDirOverlapped(vFolder.VirtualPath, cleanedVPath, false, "/") {
return nil, util.NewValidationError(fmt.Sprintf("invalid virtual folder %q, it overlaps with virtual folder %q", return nil, util.NewI18nError(
v.VirtualPath, vFolder.VirtualPath)) util.NewValidationError(fmt.Sprintf("invalid virtual folder %q, it overlaps with virtual folder %q",
v.VirtualPath, vFolder.VirtualPath)),
util.I18nErrorOverlappedFolders,
)
} }
} }
virtualFolders = append(virtualFolders, vfs.VirtualFolder{ virtualFolders = append(virtualFolders, vfs.VirtualFolder{
@ -2794,14 +2818,14 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
func validatePermissions(user *User) error { func validatePermissions(user *User) error {
if len(user.Permissions) == 0 { if len(user.Permissions) == 0 {
return util.NewValidationError("please grant some permissions to this user") return util.NewI18nError(util.NewValidationError("please grant some permissions to this user"), util.I18nErrorNoPermission)
} }
if _, ok := user.Permissions["/"]; !ok { if _, ok := user.Permissions["/"]; !ok {
return util.NewValidationError("permissions for the root dir \"/\" must be set") return util.NewI18nError(util.NewValidationError("permissions for the root dir \"/\" must be set"), util.I18nErrorNoRootPermission)
} }
permissions, err := validateUserPermissions(user.Permissions) permissions, err := validateUserPermissions(user.Permissions)
if err != nil { if err != nil {
return err return util.NewI18nError(err, util.I18nErrorGenericPermission)
} }
user.Permissions = permissions user.Permissions = permissions
return nil return nil
@ -2836,10 +2860,16 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
for _, f := range baseFilters.FilePatterns { for _, f := range baseFilters.FilePatterns {
cleanedPath := filepath.ToSlash(path.Clean(f.Path)) cleanedPath := filepath.ToSlash(path.Clean(f.Path))
if !path.IsAbs(cleanedPath) { if !path.IsAbs(cleanedPath) {
return util.NewValidationError(fmt.Sprintf("invalid path %q for file patterns filter", f.Path)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("invalid path %q for file patterns filter", f.Path)),
util.I18nErrorFilePatternPathInvalid,
)
} }
if util.Contains(filteredPaths, cleanedPath) { if util.Contains(filteredPaths, cleanedPath) {
return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
util.I18nErrorFilePatternDuplicated,
)
} }
if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 { if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 {
return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %q", f.Path)) return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %q", f.Path))
@ -2853,14 +2883,20 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
for _, pattern := range f.AllowedPatterns { for _, pattern := range f.AllowedPatterns {
_, err := path.Match(pattern, "abc") _, err := path.Match(pattern, "abc")
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)),
util.I18nErrorFilePatternInvalid,
)
} }
allowed = append(allowed, strings.ToLower(pattern)) allowed = append(allowed, strings.ToLower(pattern))
} }
for _, pattern := range f.DeniedPatterns { for _, pattern := range f.DeniedPatterns {
_, err := path.Match(pattern, "abc") _, err := path.Match(pattern, "abc")
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)),
util.I18nErrorFilePatternInvalid,
)
} }
denied = append(denied, strings.ToLower(pattern)) denied = append(denied, strings.ToLower(pattern))
} }
@ -2964,10 +3000,10 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
func validateBaseFilters(filters *sdk.BaseUserFilters) error { func validateBaseFilters(filters *sdk.BaseUserFilters) error {
checkEmptyFiltersStruct(filters) checkEmptyFiltersStruct(filters)
if err := validateIPFilters(filters); err != nil { if err := validateIPFilters(filters); err != nil {
return err return util.NewI18nError(err, util.I18nErrorIPFiltersInvalid)
} }
if err := validateBandwidthLimitsFilter(filters); err != nil { if err := validateBandwidthLimitsFilter(filters); err != nil {
return err return util.NewI18nError(err, util.I18nErrorSourceBWLimitInvalid)
} }
if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) { if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
return util.NewValidationError("invalid denied_login_methods") return util.NewValidationError("invalid denied_login_methods")
@ -2991,8 +3027,11 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
} }
} }
if filters.MaxSharesExpiration > 0 && filters.MaxSharesExpiration < filters.DefaultSharesExpiration { if filters.MaxSharesExpiration > 0 && filters.MaxSharesExpiration < filters.DefaultSharesExpiration {
return util.NewValidationError(fmt.Sprintf("default shares expiration: %d must be less than or equal to max shares expiration: %d", return util.NewI18nError(
filters.DefaultSharesExpiration, filters.MaxSharesExpiration)) util.NewValidationError(fmt.Sprintf("default shares expiration: %d must be less than or equal to max shares expiration: %d",
filters.DefaultSharesExpiration, filters.MaxSharesExpiration)),
util.I18nErrorShareExpirationInvalid,
)
} }
updateFiltersValues(filters) updateFiltersValues(filters)
@ -3001,40 +3040,54 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
func validateCombinedUserFilters(user *User) error { func validateCombinedUserFilters(user *User) error {
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) { if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
return util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration") return util.NewI18nError(
util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
util.I18nErrorDisableActive2FA,
)
} }
if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) { if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
return util.NewValidationError("you cannot require password change and at the same time disallow it") return util.NewI18nError(
util.NewValidationError("you cannot require password change and at the same time disallow it"),
util.I18nErrorPwdChangeConflict,
)
} }
return nil return nil
} }
func validateBaseParams(user *User) error { func validateBaseParams(user *User) error {
if user.Username == "" { if user.Username == "" {
return util.NewValidationError("username is mandatory") return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
} }
if err := checkReservedUsernames(user.Username); err != nil { if err := checkReservedUsernames(user.Username); err != nil {
return err return util.NewI18nError(err, util.I18nErrorReservedUsername)
} }
if user.Email != "" && !util.IsEmailValid(user.Email) { if user.Email != "" && !util.IsEmailValid(user.Email) {
return util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email)),
util.I18nErrorInvalidEmail,
)
} }
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) { if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", return util.NewI18nError(
user.Username)) util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", user.Username)),
util.I18nErrorInvalidUser,
)
} }
if user.hasRedactedSecret() { if user.hasRedactedSecret() {
return util.NewValidationError("cannot save a user with a redacted secret") return util.NewValidationError("cannot save a user with a redacted secret")
} }
if user.HomeDir == "" { if user.HomeDir == "" {
return util.NewValidationError("home_dir is mandatory") return util.NewI18nError(util.NewValidationError("home_dir is mandatory"), util.I18nErrorHomeRequired)
} }
// we can have users with no passwords and public keys, they can authenticate via SSH user certs or OIDC // we can have users with no passwords and public keys, they can authenticate via SSH user certs or OIDC
/*if user.Password == "" && len(user.PublicKeys) == 0 { /*if user.Password == "" && len(user.PublicKeys) == 0 {
return util.NewValidationError("please set a password or at least a public_key") return util.NewValidationError("please set a password or at least a public_key")
}*/ }*/
if !filepath.IsAbs(user.HomeDir) { if !filepath.IsAbs(user.HomeDir) {
return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)),
util.I18nErrorHomeInvalid,
)
} }
if user.DownloadBandwidth < 0 { if user.DownloadBandwidth < 0 {
user.DownloadBandwidth = 0 user.DownloadBandwidth = 0
@ -3050,7 +3103,11 @@ func validateBaseParams(user *User) error {
if user.Filters.IsAnonymous { if user.Filters.IsAnonymous {
user.setAnonymousSettings() user.setAnonymousSettings()
} }
return user.FsConfig.Validate(user.GetEncryptionAdditionalData()) err := user.FsConfig.Validate(user.GetEncryptionAdditionalData())
if err != nil {
return util.NewI18nError(err, util.I18nErrorFsValidation)
}
return nil
} }
func hashPlainPassword(plainPwd string) (string, error) { func hashPlainPassword(plainPwd string) (string, error) {
@ -3072,7 +3129,7 @@ func createUserPasswordHash(user *User) error {
if user.Password != "" && !user.IsPasswordHashed() { if user.Password != "" && !user.IsPasswordHashed() {
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 { if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil { if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {
return util.NewValidationError(err.Error()) return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
} }
} }
hashedPwd, err := hashPlainPassword(user.Password) hashedPwd, err := hashPlainPassword(user.Password)
@ -3127,10 +3184,10 @@ func ValidateUser(user *User) error {
return err return err
} }
if err := validateUserTOTPConfig(&user.Filters.TOTPConfig, user.Username); err != nil { if err := validateUserTOTPConfig(&user.Filters.TOTPConfig, user.Username); err != nil {
return err return util.NewI18nError(err, util.I18nError2FAInvalid)
} }
if err := validateUserRecoveryCodes(user); err != nil { if err := validateUserRecoveryCodes(user); err != nil {
return err return util.NewI18nError(err, util.I18nErrorRecoveryCodesInvalid)
} }
vfolders, err := validateAssociatedVirtualFolders(user.VirtualFolders) vfolders, err := validateAssociatedVirtualFolders(user.VirtualFolders)
if err != nil { if err != nil {
@ -3144,7 +3201,7 @@ func ValidateUser(user *User) error {
return err return err
} }
if err := validatePublicKeys(user); err != nil { if err := validatePublicKeys(user); err != nil {
return err return util.NewI18nError(err, util.I18nErrorPubKeyInvalid)
} }
if err := validateBaseFilters(&user.Filters.BaseUserFilters); err != nil { if err := validateBaseFilters(&user.Filters.BaseUserFilters); err != nil {
return err return err

View file

@ -76,19 +76,6 @@ type Share struct {
IsRestore bool `json:"-"` IsRestore bool `json:"-"`
} }
// GetScopeAsString returns the share's scope as string.
// Used in web pages
func (s *Share) GetScopeAsString() string {
switch s.Scope {
case ShareScopeWrite:
return "Write"
case ShareScopeReadWrite:
return "Read/Write"
default:
return "Read"
}
}
// IsExpired returns true if the share is expired // IsExpired returns true if the share is expired
func (s *Share) IsExpired() bool { func (s *Share) IsExpired() bool {
if s.ExpiresAt > 0 { if s.ExpiresAt > 0 {
@ -97,31 +84,6 @@ func (s *Share) IsExpired() bool {
return false return false
} }
// GetInfoString returns share's info as string.
func (s *Share) GetInfoString() string {
var result strings.Builder
if s.ExpiresAt > 0 {
t := util.GetTimeFromMsecSinceEpoch(s.ExpiresAt)
result.WriteString(fmt.Sprintf("Expiration: %v. ", t.Format("2006-01-02 15:04"))) // YYYY-MM-DD HH:MM
}
if s.LastUseAt > 0 {
t := util.GetTimeFromMsecSinceEpoch(s.LastUseAt)
result.WriteString(fmt.Sprintf("Last use: %v. ", t.Format("2006-01-02 15:04")))
}
if s.MaxTokens > 0 {
result.WriteString(fmt.Sprintf("Usage: %v/%v. ", s.UsedTokens, s.MaxTokens))
} else {
result.WriteString(fmt.Sprintf("Used tokens: %v. ", s.UsedTokens))
}
if len(s.AllowFrom) > 0 {
result.WriteString(fmt.Sprintf("Allowed IP/Mask: %v. ", len(s.AllowFrom)))
}
if s.Password != "" {
result.WriteString("Password protected.")
}
return result.String()
}
// GetAllowedFromAsString returns the allowed IP as comma separated string // GetAllowedFromAsString returns the allowed IP as comma separated string
func (s *Share) GetAllowedFromAsString() string { func (s *Share) GetAllowedFromAsString() string {
return strings.Join(s.AllowFrom, ",") return strings.Join(s.AllowFrom, ",")
@ -185,7 +147,7 @@ func (s *Share) hashPassword() error {
} }
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 { if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
if err := passwordvalidator.Validate(s.Password, minEntropy); err != nil { if err := passwordvalidator.Validate(s.Password, minEntropy); err != nil {
return util.NewValidationError(err.Error()) return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
} }
} }
if config.PasswordHashing.Algo == HashingAlgoBcrypt { if config.PasswordHashing.Algo == HashingAlgoBcrypt {
@ -214,14 +176,14 @@ func (s *Share) validatePaths() error {
} }
s.Paths = paths s.Paths = paths
if len(s.Paths) == 0 { if len(s.Paths) == 0 {
return util.NewValidationError("at least a shared path is required") return util.NewI18nError(util.NewValidationError("at least a shared path is required"), util.I18nErrorSharePathRequired)
} }
for idx := range s.Paths { for idx := range s.Paths {
s.Paths[idx] = util.CleanPath(s.Paths[idx]) s.Paths[idx] = util.CleanPath(s.Paths[idx])
} }
s.Paths = util.RemoveDuplicates(s.Paths, false) s.Paths = util.RemoveDuplicates(s.Paths, false)
if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 { if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 {
return util.NewValidationError("the write share scope requires exactly one path") return util.NewI18nError(util.NewValidationError("the write share scope requires exactly one path"), util.I18nErrorShareWriteScope)
} }
// check nested paths // check nested paths
if len(s.Paths) > 1 { if len(s.Paths) > 1 {
@ -230,8 +192,8 @@ func (s *Share) validatePaths() error {
if idx == innerIdx { if idx == innerIdx {
continue continue
} }
if util.IsDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true, "/") { if s.Paths[idx] == "/" || s.Paths[innerIdx] == "/" || util.IsDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true, "/") {
return util.NewGenericError("shared paths cannot be nested") return util.NewI18nError(util.NewGenericError("shared paths cannot be nested"), util.I18nErrorShareNestedPaths)
} }
} }
} }
@ -244,26 +206,26 @@ func (s *Share) validate() error {
return util.NewValidationError("share_id is mandatory") return util.NewValidationError("share_id is mandatory")
} }
if s.Name == "" { if s.Name == "" {
return util.NewValidationError("name is mandatory") return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
} }
if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite { if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite {
return util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)) return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)), util.I18nErrorShareScope)
} }
if err := s.validatePaths(); err != nil { if err := s.validatePaths(); err != nil {
return err return err
} }
if s.ExpiresAt > 0 { if s.ExpiresAt > 0 {
if !s.IsRestore && s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) { if !s.IsRestore && s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
return util.NewValidationError("expiration must be in the future") return util.NewI18nError(util.NewValidationError("expiration must be in the future"), util.I18nErrorShareExpirationPast)
} }
} else { } else {
s.ExpiresAt = 0 s.ExpiresAt = 0
} }
if s.MaxTokens < 0 { if s.MaxTokens < 0 {
return util.NewValidationError("invalid max tokens") return util.NewI18nError(util.NewValidationError("invalid max tokens"), util.I18nErrorShareMaxTokens)
} }
if s.Username == "" { if s.Username == "" {
return util.NewValidationError("username is mandatory") return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
} }
if s.HasRedactedPassword() { if s.HasRedactedPassword() {
return util.NewValidationError("cannot save a share with a redacted password") return util.NewValidationError("cannot save a share with a redacted password")
@ -275,7 +237,10 @@ func (s *Share) validate() error {
for _, IPMask := range s.AllowFrom { for _, IPMask := range s.AllowFrom {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse allow from entry %q : %v", IPMask, err)) return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("could not parse allow from entry %q : %v", IPMask, err)),
util.I18nErrorInvalidIPMask,
)
} }
} }
return nil return nil
@ -313,11 +278,11 @@ func (s *Share) GetRelativePath(name string) string {
// IsUsable checks if the share is usable from the specified IP // IsUsable checks if the share is usable from the specified IP
func (s *Share) IsUsable(ip string) (bool, error) { func (s *Share) IsUsable(ip string) (bool, error) {
if s.MaxTokens > 0 && s.UsedTokens >= s.MaxTokens { if s.MaxTokens > 0 && s.UsedTokens >= s.MaxTokens {
return false, util.NewRecordNotFoundError("max share usage exceeded") return false, util.NewI18nError(util.NewRecordNotFoundError("max share usage exceeded"), util.I18nErrorShareUsage)
} }
if s.ExpiresAt > 0 { if s.ExpiresAt > 0 {
if s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) { if s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
return false, util.NewRecordNotFoundError("share expired") return false, util.NewI18nError(util.NewRecordNotFoundError("share expired"), util.I18nErrorShareExpired)
} }
} }
if len(s.AllowFrom) == 0 { if len(s.AllowFrom) == 0 {
@ -325,7 +290,7 @@ func (s *Share) IsUsable(ip string) (bool, error) {
} }
parsedIP := net.ParseIP(ip) parsedIP := net.ParseIP(ip)
if parsedIP == nil { if parsedIP == nil {
return false, ErrLoginNotAllowedFromIP return false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied)
} }
for _, ipMask := range s.AllowFrom { for _, ipMask := range s.AllowFrom {
_, network, err := net.ParseCIDR(ipMask) _, network, err := net.ParseCIDR(ipMask)
@ -336,5 +301,5 @@ func (s *Share) IsUsable(ip string) (bool, error) {
return true, nil return true, nil
} }
} }
return false, ErrLoginNotAllowedFromIP return false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied)
} }

View file

@ -532,22 +532,28 @@ func changeUserPassword(w http.ResponseWriter, r *http.Request) {
func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error {
if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" {
return util.NewValidationError("please provide the current password and the new one two times") return util.NewI18nError(
util.NewValidationError("please provide the current password and the new one two times"),
util.I18nErrorChangePwdRequiredFields,
)
} }
if newPassword != confirmNewPassword { if newPassword != confirmNewPassword {
return util.NewValidationError("the two password fields do not match") return util.NewI18nError(util.NewValidationError("the two password fields do not match"), util.I18nErrorChangePwdNoMatch)
} }
if currentPassword == newPassword { if currentPassword == newPassword {
return util.NewValidationError("the new password must be different from the current one") return util.NewI18nError(
util.NewValidationError("the new password must be different from the current one"),
util.I18nErrorChangePwdNoDifferent,
)
} }
claims, err := getTokenClaims(r) claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" { if err != nil || claims.Username == "" {
return errors.New("invalid token claims") return util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)
} }
_, err = dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr), _, err = dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr),
getProtocolFromRequest(r)) getProtocolFromRequest(r))
if err != nil { if err != nil {
return util.NewValidationError("current password does not match") return util.NewI18nError(util.NewValidationError("current password does not match"), util.I18nErrorChangePwdCurrentNoMatch)
} }
return dataprovider.UpdateUserPassword(claims.Username, newPassword, dataprovider.ActionExecutorSelf, return dataprovider.UpdateUserPassword(claims.Username, newPassword, dataprovider.ActionExecutorSelf,

View file

@ -455,7 +455,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
isWebClient := isWebClientRequest(r) isWebClient := isWebClientRequest(r)
renderError := func(err error, message string, statusCode int) { renderError := func(err error, message string, statusCode int) {
if isWebClient { if isWebClient {
s.renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "") s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, statusCode, err, message)
} else { } else {
sendAPIResponse(w, r, err, message, statusCode) sendAPIResponse(w, r, err, message, statusCode)
} }
@ -466,14 +466,15 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
if err != nil { if err != nil {
statusCode := getRespStatus(err) statusCode := getRespStatus(err)
if statusCode == http.StatusNotFound { if statusCode == http.StatusNotFound {
err = errors.New("share does not exist") err = util.NewI18nError(errors.New("share does not exist"), util.I18nError404Message)
} }
renderError(err, "", statusCode) renderError(err, "", statusCode)
return share, nil, err return share, nil, err
} }
if !util.Contains(validScopes, share.Scope) { if !util.Contains(validScopes, share.Scope) {
renderError(nil, "Invalid share scope", http.StatusForbidden) err := errors.New("invalid share scope")
return share, nil, errors.New("invalid share scope") renderError(util.NewI18nError(err, util.I18nErrorShareScope), "", http.StatusForbidden)
return share, nil, err
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
ok, err := share.IsUsable(ipAddr) ok, err := share.IsUsable(ipAddr)
@ -524,20 +525,29 @@ func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
return user, err return user, err
} }
if !user.CanManageShares() { if !user.CanManageShares() {
return user, util.NewRecordNotFoundError("this share does not exist") return user, util.NewI18nError(util.NewRecordNotFoundError("this share does not exist"), util.I18nError404Message)
} }
if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) { if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
return user, fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission) return user, util.NewI18nError(
fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission),
util.I18nError403Message,
)
} }
if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) { if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) {
return user, util.NewMethodDisabledError("two-factor authentication requirements not met") return user, util.NewI18nError(
util.NewMethodDisabledError("two-factor authentication requirements not met"),
util.I18nError403Message,
)
} }
return user, nil return user, nil
} }
func validateBrowsableShare(share dataprovider.Share, connection *Connection) error { func validateBrowsableShare(share dataprovider.Share, connection *Connection) error {
if len(share.Paths) != 1 { if len(share.Paths) != 1 {
return util.NewValidationError("a share with multiple paths is not browsable") return util.NewI18nError(
util.NewValidationError("a share with multiple paths is not browsable"),
util.I18nErrorShareBrowsePaths,
)
} }
basePath := share.Paths[0] basePath := share.Paths[0]
info, err := connection.Stat(basePath, 0) info, err := connection.Stat(basePath, 0)
@ -545,7 +555,10 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er
return fmt.Errorf("unable to check the share directory: %w", err) return fmt.Errorf("unable to check the share directory: %w", err)
} }
if !info.IsDir() { if !info.IsDir() {
return util.NewValidationError("the shared object is not a directory and so it is not browsable") return util.NewI18nError(
util.NewValidationError("the shared object is not a directory and so it is not browsable"),
util.I18nErrorShareBrowseNoDir,
)
} }
return nil return nil
} }
@ -556,7 +569,10 @@ func getBrowsableSharedPath(share dataprovider.Share, r *http.Request) (string,
return name, nil return name, nil
} }
if name != share.Paths[0] && !strings.HasPrefix(name, share.Paths[0]+"/") { if name != share.Paths[0] && !strings.HasPrefix(name, share.Paths[0]+"/") {
return "", util.NewValidationError(fmt.Sprintf("Invalid path %q", r.URL.Query().Get("path"))) return "", util.NewI18nError(
util.NewValidationError(fmt.Sprintf("Invalid path %q", r.URL.Query().Get("path"))),
util.I18nErrorPathInvalid,
)
} }
return name, nil return name, nil
} }

View file

@ -628,23 +628,32 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error { func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error {
if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) { if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username) logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username)
return fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username) return util.NewI18nError(
fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username),
util.I18nErrorProtocolForbidden,
)
} }
if !isLoggedInWithOIDC(r) && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) { if !isLoggedInWithOIDC(r) && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {
logger.Info(logSender, connectionID, "cannot login user %q, password login method is not allowed", user.Username) logger.Info(logSender, connectionID, "cannot login user %q, password login method is not allowed", user.Username)
return fmt.Errorf("login method password is not allowed for user %q", user.Username) return util.NewI18nError(
fmt.Errorf("login method password is not allowed for user %q", user.Username),
util.I18nErrorPwdLoginForbidden,
)
} }
if checkSessions && user.MaxSessions > 0 { if checkSessions && user.MaxSessions > 0 {
activeSessions := common.Connections.GetActiveSessions(user.Username) activeSessions := common.Connections.GetActiveSessions(user.Username)
if activeSessions >= user.MaxSessions { if activeSessions >= user.MaxSessions {
logger.Info(logSender, connectionID, "authentication refused for user: %q, too many open sessions: %v/%v", user.Username, logger.Info(logSender, connectionID, "authentication refused for user: %q, too many open sessions: %v/%v", user.Username,
activeSessions, user.MaxSessions) activeSessions, user.MaxSessions)
return fmt.Errorf("too many open sessions: %v", activeSessions) return util.NewI18nError(fmt.Errorf("too many open sessions: %v", activeSessions), util.I18nError429Message)
} }
} }
if !user.IsLoginFromAddrAllowed(r.RemoteAddr) { if !user.IsLoginFromAddrAllowed(r.RemoteAddr) {
logger.Info(logSender, connectionID, "cannot login user %q, remote address is not allowed: %v", user.Username, r.RemoteAddr) logger.Info(logSender, connectionID, "cannot login user %q, remote address is not allowed: %v", user.Username, r.RemoteAddr)
return fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, r.RemoteAddr) return util.NewI18nError(
fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, r.RemoteAddr),
util.I18nErrorIPForbidden,
)
} }
return nil return nil
} }
@ -656,7 +665,7 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
var user dataprovider.User var user dataprovider.User
if username == "" { if username == "" {
return util.NewValidationError("username is mandatory") return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
} }
if isAdmin { if isAdmin {
admin, err = dataprovider.AdminExists(username) admin, err = dataprovider.AdminExists(username)
@ -668,7 +677,10 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
subject = fmt.Sprintf("Email Verification Code for user %q", username) subject = fmt.Sprintf("Email Verification Code for user %q", username)
if err == nil { if err == nil {
if !isUserAllowedToResetPassword(r, &user) { if !isUserAllowedToResetPassword(r, &user) {
return util.NewValidationError("you are not allowed to reset your password") return util.NewI18nError(
util.NewValidationError("you are not allowed to reset your password"),
util.I18nErrorPwdResetForbidded,
)
} }
} }
} }
@ -679,10 +691,13 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
username, isAdmin) username, isAdmin)
return nil return nil
} }
return util.NewGenericError("Error retrieving your account, please try again later") return util.NewI18nError(util.NewGenericError("Error retrieving your account, please try again later"), util.I18nErrorGetUser)
} }
if email == "" { if email == "" {
return util.NewValidationError("Your account does not have an email address, it is not possible to reset your password by sending an email verification code") return util.NewI18nError(
util.NewValidationError("Your account does not have an email address, it is not possible to reset your password by sending an email verification code"),
util.I18nErrorPwdResetNoEmail,
)
} }
c := newResetCode(username, isAdmin) c := newResetCode(username, isAdmin)
body := new(bytes.Buffer) body := new(bytes.Buffer)
@ -696,7 +711,10 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
if err := smtp.SendEmail([]string{email}, nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil { if err := smtp.SendEmail([]string{email}, nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
logger.Warn(logSender, middleware.GetReqID(r.Context()), "unable to send password reset code via email: %v, elapsed: %v", logger.Warn(logSender, middleware.GetReqID(r.Context()), "unable to send password reset code via email: %v, elapsed: %v",
err, time.Since(startTime)) err, time.Since(startTime))
return util.NewGenericError(fmt.Sprintf("Unable to send confirmation code via email: %v", err)) return util.NewI18nError(
util.NewGenericError(fmt.Sprintf("Error sending confirmation code via email: %v", err)),
util.I18nErrorPwdResetSendEmail,
)
} }
logger.Debug(logSender, middleware.GetReqID(r.Context()), "reset code sent via email to %q, email: %q, is admin? %v, elapsed: %v", logger.Debug(logSender, middleware.GetReqID(r.Context()), "reset code sent via email to %q, email: %q, is admin? %v, elapsed: %v",
username, email, isAdmin, time.Since(startTime)) username, email, isAdmin, time.Since(startTime))
@ -744,7 +762,10 @@ func handleResetPassword(r *http.Request, code, newPassword string, isAdmin bool
} }
if err == nil { if err == nil {
if !isUserAllowedToResetPassword(r, &user) { if !isUserAllowedToResetPassword(r, &user) {
return &admin, &user, util.NewValidationError("you are not allowed to reset your password") return &admin, &user, util.NewI18nError(
util.NewValidationError("you are not allowed to reset your password"),
util.I18nErrorPwdResetForbidded,
)
} }
} }
err = dataprovider.UpdateUserPassword(user.Username, newPassword, dataprovider.ActionExecutorSelf, err = dataprovider.UpdateUserPassword(user.Username, newPassword, dataprovider.ActionExecutorSelf,

View file

@ -100,16 +100,16 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io
transferQuota := c.GetTransferQuota() transferQuota := c.GetTransferQuota()
if !transferQuota.HasDownloadSpace() { if !transferQuota.HasDownloadSpace() {
c.Log(logger.LevelInfo, "denying file read due to quota limits") c.Log(logger.LevelInfo, "denying file read due to quota limits")
return nil, c.GetReadQuotaExceededError() return nil, util.NewI18nError(c.GetReadQuotaExceededError(), util.I18nErrorQuotaRead)
} }
if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) { if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) {
return nil, c.GetPermissionDeniedError() return nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)
} }
if ok, policy := c.User.IsFileAllowed(name); !ok { if ok, policy := c.User.IsFileAllowed(name); !ok {
c.Log(logger.LevelWarn, "reading file %q is not allowed", name) c.Log(logger.LevelWarn, "reading file %q is not allowed", name)
return nil, c.GetErrorForDeniedFile(policy) return nil, util.NewI18nError(c.GetErrorForDeniedFile(policy), util.I18nError403Message)
} }
fs, p, err := c.GetFsAndResolvedPath(name) fs, p, err := c.GetFsAndResolvedPath(name)
@ -120,7 +120,7 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io
if method != http.MethodHead { if method != http.MethodHead {
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil { if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil {
c.Log(logger.LevelDebug, "download for file %q denied by pre action: %v", name, err) c.Log(logger.LevelDebug, "download for file %q denied by pre action: %v", name, err)
return nil, c.GetPermissionDeniedError() return nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message)
} }
} }

View file

@ -3209,7 +3209,7 @@ func TestMustChangePasswordRequirement(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Password change required. Please set a new password to continue to use your account") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdRequired)
// change pwd // change pwd
pwd := make(map[string]string) pwd := make(map[string]string)
pwd["current_password"] = defaultPassword pwd["current_password"] = defaultPassword
@ -3317,7 +3317,7 @@ func TestTwoFactorRequirements(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols") assert.Contains(t, rr.Body.String(), util.I18nError2FARequired)
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username) configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username)
assert.NoError(t, err) assert.NoError(t, err)
@ -6425,7 +6425,7 @@ func TestNamingRules(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "the following characters are allowed") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser)
// test user reset password. Setting the new password will fail because the username is not valid // test user reset password. Setting the new password will fail because the username is not valid
form = make(url.Values) form = make(url.Values)
form.Set("username", user.Username) form.Set("username", user.Username)
@ -6449,7 +6449,7 @@ func TestNamingRules(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "the following characters are allowed") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser)
adminAPIToken, err = getJWTAPITokenFromTestServer(admin.Username, defaultTokenAuthPass) adminAPIToken, err = getJWTAPITokenFromTestServer(admin.Username, defaultTokenAuthPass)
assert.NoError(t, err) assert.NoError(t, err)
@ -6743,7 +6743,7 @@ func TestSaveErrors(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusInternalServerError, rr.Code) assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Contains(t, rr.Body.String(), "unable to set the recovery code as used") assert.Contains(t, rr.Body.String(), util.I18nError500Message)
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -6886,7 +6886,7 @@ func TestProviderErrors(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Error retrieving your account, please try again later") assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)
req, err = http.NewRequest(http.MethodGet, webClientSharesPath, nil) req, err = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -9615,7 +9615,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
form.Set("passcode", "invalid_user_passcode") form.Set("passcode", "invalid_user_passcode")
@ -9626,7 +9626,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid authentication code") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
form.Set("passcode", "") form.Set("passcode", "")
req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))
@ -9636,7 +9636,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid credentials") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
form.Set("passcode", passcode) form.Set("passcode", passcode)
req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))
@ -9677,7 +9677,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
form.Set("recovery_code", "") form.Set("recovery_code", "")
@ -9688,7 +9688,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid credentials") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
form.Set("recovery_code", recoveryCode) form.Set("recovery_code", recoveryCode)
req, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))
@ -9764,7 +9764,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "This recovery code was already used") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
form.Set("recovery_code", "invalid_user_recovery_code") form.Set("recovery_code", "invalid_user_recovery_code")
req, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))
@ -9774,7 +9774,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid recovery code") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
form = getLoginForm(defaultUsername, defaultPassword, csrfToken) form = getLoginForm(defaultUsername, defaultPassword, csrfToken)
req, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))
@ -9815,7 +9815,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Two factory authentication is not enabled") assert.Contains(t, rr.Body.String(), util.I18n2FADisabled)
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -9829,7 +9829,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid credentials") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err) assert.NoError(t, err)
@ -9838,7 +9838,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid credentials") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
req, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil) req, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -10880,7 +10880,7 @@ func TestPermGroupOverride(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols") assert.Contains(t, rr.Body.String(), util.I18nError2FARequired)
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -12333,14 +12333,14 @@ func TestWebClientLoginMock(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr) checkResponseCode(t, http.StatusNotFound, rr)
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)
req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil) req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil)
req.RemoteAddr = defaultRemoteAddr req.RemoteAddr = defaultRemoteAddr
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr) checkResponseCode(t, http.StatusNotFound, rr)
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") assert.Contains(t, rr.Body.String(), util.I18nErrorDirListUser)
form := make(url.Values) form := make(url.Values)
form.Set("files", `[]`) form.Set("files", `[]`)
@ -12351,7 +12351,7 @@ func TestWebClientLoginMock(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr) checkResponseCode(t, http.StatusNotFound, rr)
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)
req, _ = http.NewRequest(http.MethodGet, userDirsPath, nil) req, _ = http.NewRequest(http.MethodGet, userDirsPath, nil)
setBearerForReq(req, apiUserToken) setBearerForReq(req, apiUserToken)
@ -12389,7 +12389,7 @@ func TestWebClientLoginErrorsMock(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := executeRequest(req) rr := executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid credentials") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
form = getLoginForm(defaultUsername, defaultPassword, "") form = getLoginForm(defaultUsername, defaultPassword, "")
req, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode())))
@ -12397,7 +12397,7 @@ func TestWebClientLoginErrorsMock(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
} }
func TestWebClientMaxConnections(t *testing.T) { func TestWebClientMaxConnections(t *testing.T) {
@ -12514,7 +12514,7 @@ func TestDefender(t *testing.T) {
req.RemoteAddr = remoteAddr req.RemoteAddr = remoteAddr
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "your IP address is blocked") assert.Contains(t, rr.Body.String(), util.I18nErrorIPForbidden)
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
req.RequestURI = webUsersPath req.RequestURI = webUsersPath
@ -12636,35 +12636,35 @@ func TestMaxSessions(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
req, err = http.NewRequest(http.MethodGet, webClientDirsPath, nil) req, err = http.NewRequest(http.MethodGet, webClientDirsPath, nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nErrorDirList429)
req, err = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=p", nil) req, err = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=p", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=file", nil) //nolint:goconst req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=file", nil) //nolint:goconst
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
req, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+"?path=file", nil) req, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+"?path=file", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
// test reset password // test reset password
smtpCfg := smtp.Config{ smtpCfg := smtp.Config{
@ -12698,7 +12698,7 @@ func TestMaxSessions(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Password reset successfully but unable to login") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
smtpCfg = smtp.Config{} smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir, true) err = smtpCfg.Initialize(configDir, true)
@ -13012,7 +13012,7 @@ func TestSFTPLoopError(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Password reset successfully but unable to login") assert.Contains(t, rr.Body.String(), util.I18nErrorLoginAfterReset)
smtpCfg = smtp.Config{} smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir, true) err = smtpCfg.Initialize(configDir, true)
@ -13090,7 +13090,7 @@ func TestWebClientChangePwd(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))
@ -13099,7 +13099,7 @@ func TestWebClientChangePwd(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "the new password must be different from the current one") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoDifferent)
form.Set("current_password", defaultPassword+"2") form.Set("current_password", defaultPassword+"2")
form.Set("new_password1", defaultPassword+"1") form.Set("new_password1", defaultPassword+"1")
@ -13110,7 +13110,7 @@ func TestWebClientChangePwd(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "current password does not match") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdCurrentNoMatch)
form.Set("current_password", defaultPassword) form.Set("current_password", defaultPassword)
form.Set("new_password1", defaultPassword+"1") form.Set("new_password1", defaultPassword+"1")
@ -13203,7 +13203,7 @@ func TestPreDownloadHook(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "permission denied") assert.Contains(t, rr.Body.String(), util.I18nError403Message)
req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -13387,7 +13387,7 @@ func TestShareUsage(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Invalid share scope") assert.Contains(t, rr.Body.String(), "invalid share scope")
share.MaxTokens = 3 share.MaxTokens = 3
share.Scope = dataprovider.ShareScopeWrite share.Scope = dataprovider.ShareScopeWrite
@ -13652,7 +13652,7 @@ func TestShareMaxExpiration(t *testing.T) {
setJWTCookieForReq(req, webClientToken) setJWTCookieForReq(req, webClientToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "share must expire before") assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpirationOutOfRange)
req, err = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, shareID), bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, shareID), bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err) assert.NoError(t, err)
@ -13661,7 +13661,7 @@ func TestShareMaxExpiration(t *testing.T) {
setJWTCookieForReq(req, webClientToken) setJWTCookieForReq(req, webClientToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "share must expire before") assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpirationOutOfRange)
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -13675,7 +13675,7 @@ func TestShareMaxExpiration(t *testing.T) {
setJWTCookieForReq(req, webClientToken) setJWTCookieForReq(req, webClientToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)
} }
func TestWebClientShareCredentials(t *testing.T) { func TestWebClientShareCredentials(t *testing.T) {
@ -13748,7 +13748,7 @@ func TestWebClientShareCredentials(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
// set the CSRF token // set the CSRF token
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath) csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
assert.NoError(t, err) assert.NoError(t, err)
@ -13759,7 +13759,7 @@ func TestWebClientShareCredentials(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Share login successful") assert.Contains(t, rr.Body.String(), util.I18nShareLoginOK)
cookie := rr.Header().Get("Set-Cookie") cookie := rr.Header().Get("Set-Cookie")
cookie = strings.TrimPrefix(cookie, "jwt=") cookie = strings.TrimPrefix(cookie, "jwt=")
assert.NotEmpty(t, cookie) assert.NotEmpty(t, cookie)
@ -13806,7 +13806,7 @@ func TestWebClientShareCredentials(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), dataprovider.ErrInvalidCredentials.Error()) assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
// login with the next param set // login with the next param set
form.Set("share_password", defaultPassword) form.Set("share_password", defaultPassword)
nextURI := path.Join(webClientPubSharesPath, shareReadID, "browse") nextURI := path.Join(webClientPubSharesPath, shareReadID, "browse")
@ -13826,7 +13826,7 @@ func TestWebClientShareCredentials(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), dataprovider.ErrInvalidCredentials.Error()) assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials)
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -13885,13 +13885,13 @@ func TestShareMaxSessions(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
req, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+"/"+objectID+"/browse", nil) req, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+"/"+objectID+"/browse", nil)
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID+"/files?path=afile", nil) req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID+"/files?path=afile", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -13906,7 +13906,7 @@ func TestShareMaxSessions(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusTooManyRequests, rr) checkResponseCode(t, http.StatusTooManyRequests, rr)
assert.Contains(t, rr.Body.String(), "too many open sessions") assert.Contains(t, rr.Body.String(), util.I18nError429Message)
req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID, nil) req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID, nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -14131,7 +14131,7 @@ func TestShareReadWrite(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(defaultUsername, defaultPassword) req.SetBasicAuth(defaultUsername, defaultPassword)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusBadRequest, rr)
// invalid files list // invalid files list
form.Set("files", fmt.Sprintf(`[%s]`, testFileName)) form.Set("files", fmt.Sprintf(`[%s]`, testFileName))
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "partial"), req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "partial"),
@ -14141,8 +14141,8 @@ func TestShareReadWrite(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(defaultUsername, defaultPassword) req.SetBasicAuth(defaultUsername, defaultPassword)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to get files list") assert.Contains(t, rr.Body.String(), util.I18nError400Message)
// missing directory // missing directory
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "partial?path=missing"), req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "partial?path=missing"),
bytes.NewBuffer([]byte(form.Encode()))) bytes.NewBuffer([]byte(form.Encode())))
@ -14151,8 +14151,8 @@ func TestShareReadWrite(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(defaultUsername, defaultPassword) req.SetBasicAuth(defaultUsername, defaultPassword)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to get files list") assert.Contains(t, rr.Body.String(), util.I18nError400Message)
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("../"+testFileName), req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("../"+testFileName),
bytes.NewBuffer(content)) bytes.NewBuffer(content))
@ -14390,7 +14390,7 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Invalid share scope") assert.Contains(t, rr.Body.String(), "invalid share scope")
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -14434,7 +14434,7 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Invalid share path") assert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid)
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "dirs?path=%2F.."), nil) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "dirs?path=%2F.."), nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -14467,7 +14467,7 @@ func TestBrowseShares(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Invalid share path") assert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid)
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileName), nil) //nolint:goconst req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileName), nil) //nolint:goconst
assert.NoError(t, err) assert.NoError(t, err)
@ -14487,7 +14487,7 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "file does not exist") assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path=missing"), nil) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path=missing"), nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -14508,7 +14508,7 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "non regular files are not supported for shares") assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileNameLink), nil) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileNameLink), nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -14520,25 +14520,25 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "does not look like a PDF") assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=missing"), nil) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=missing"), nil)
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to get file") assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=%2F"), nil) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=%2F"), nil)
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "is not a file") assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=%2F.."), nil) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=%2F.."), nil)
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Invalid share path") assert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid)
fakePDF := []byte(`%PDF-1.6`) fakePDF := []byte(`%PDF-1.6`)
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
@ -14612,13 +14612,13 @@ func TestBrowseShares(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to validate share") assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir)
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path="+testFileName), nil) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path="+testFileName), nil)
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to validate share") assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir)
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileName), nil) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileName), nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -14635,7 +14635,7 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "the shared object is not a directory and so it is not browsable") assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir)
// now test a missing shareID // now test a missing shareID
objectID = "123456" objectID = "123456"
@ -14720,7 +14720,7 @@ func TestBrowseShares(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "a share with multiple paths is not browsable") assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowsePaths)
// share the root path // share the root path
share = dataprovider.Share{ share = dataprovider.Share{
Name: "test share root", Name: "test share root",
@ -15366,14 +15366,14 @@ func TestWebClientViewPDF(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to get file") assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)
req, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+"?path=%2F", nil) req, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+"?path=%2F", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Invalid file") assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)
err = os.WriteFile(filepath.Join(user.GetHomeDir(), "test.pdf"), []byte("some text data"), 0666) err = os.WriteFile(filepath.Join(user.GetHomeDir(), "test.pdf"), []byte("some text data"), 0666)
assert.NoError(t, err) assert.NoError(t, err)
@ -15382,8 +15382,8 @@ func TestWebClientViewPDF(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusInternalServerError, rr)
assert.Contains(t, rr.Body.String(), "Invalid PDF file") assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)
err = createTestFile(filepath.Join(user.GetHomeDir(), "test.pdf"), 1024) err = createTestFile(filepath.Join(user.GetHomeDir(), "test.pdf"), 1024)
assert.NoError(t, err) assert.NoError(t, err)
@ -15393,7 +15393,7 @@ func TestWebClientViewPDF(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "does not look like a PDF") assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage)
fakePDF := []byte(`%PDF-1.6`) fakePDF := []byte(`%PDF-1.6`)
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
@ -15421,7 +15421,7 @@ func TestWebClientViewPDF(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Unable to get a reader for the file") assert.Contains(t, rr.Body.String(), util.I18nError403Message)
user.Filters.FilePatterns = []sdk.PatternsFilter{ user.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
@ -15476,21 +15476,21 @@ func TestWebEditFile(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "exceeds the maximum allowed size") assert.Contains(t, rr.Body.String(), util.I18nErrorEditSize)
req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=missing", nil) req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=missing", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to stat file") assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric)
req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=%2F", nil) req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=%2F", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "does not point to a file") assert.Contains(t, rr.Body.String(), util.I18nErrorEditDir)
user.Filters.DeniedProtocols = []string{common.ProtocolHTTP} user.Filters.DeniedProtocols = []string{common.ProtocolHTTP}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
@ -15517,7 +15517,7 @@ func TestWebEditFile(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Unable to get a reader") assert.Contains(t, rr.Body.String(), util.I18nError403Message)
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -15614,7 +15614,7 @@ func TestWebGetFiles(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusBadRequest, rr)
filesList := []string{testFileName, testDir, testFileName + extensions[2]} filesList := []string{testFileName, testDir, testFileName + extensions[2]}
asJSON, err := json.Marshal(filesList) asJSON, err := json.Marshal(filesList)
@ -15650,8 +15650,8 @@ func TestWebGetFiles(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Unable to get files list") assert.Contains(t, rr.Body.String(), util.I18nError400Message)
req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path=/", nil) //nolint:goconst req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path=/", nil) //nolint:goconst
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
@ -15675,7 +15675,7 @@ func TestWebGetFiles(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr) checkResponseCode(t, http.StatusNotFound, rr)
assert.Contains(t, rr.Body.String(), "Unable to get directory contents") assert.Contains(t, rr.Body.String(), util.I18nErrorDirListGeneric)
req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path=missing", nil) req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path=missing", nil)
setBearerForReq(req, webAPIToken) setBearerForReq(req, webAPIToken)
@ -16797,7 +16797,7 @@ func TestWebFilesTransferQuotaLimits(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Denying share read due to quota limits") assert.Contains(t, rr.Body.String(), util.I18nErrorQuotaRead)
share2 := dataprovider.Share{ share2 := dataprovider.Share{
Name: "share2", Name: "share2",
@ -17489,28 +17489,29 @@ func TestGetFilesSFTPBackend(t *testing.T) {
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
htmlErrFrag := `div id="errorMsg" class="rounded border-warning border border-dashed bg-light-warning`
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "missing"), nil) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "missing"), nil)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) assert.Contains(t, rr.Body.String(), htmlErrFrag)
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir/sub", nil) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir/sub", nil)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) assert.Contains(t, rr.Body.String(), htmlErrFrag)
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir1/afile", nil) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir1/afile", nil)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) assert.Contains(t, rr.Body.String(), htmlErrFrag)
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir2/afile.txt", nil) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir2/afile.txt", nil)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) assert.Contains(t, rr.Body.String(), htmlErrFrag)
req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
@ -17573,7 +17574,7 @@ func TestClientUserClose(t *testing.T) {
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) checkResponseCode(t, http.StatusInternalServerError, rr)
assert.Contains(t, rr.Body.String(), "Unable to read the file") assert.Contains(t, rr.Body.String(), util.I18nError500Message)
}() }()
wg.Add(1) wg.Add(1)
go func() { go func() {
@ -18015,7 +18016,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "cannot parse") assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpiration)
form.Set("expiration_date", util.GetTimeFromMsecSinceEpoch(share.ExpiresAt).UTC().Format("2006-01-02 15:04:05")) form.Set("expiration_date", util.GetTimeFromMsecSinceEpoch(share.ExpiresAt).UTC().Format("2006-01-02 15:04:05"))
form.Set("scope", "") form.Set("scope", "")
// invalid scope // invalid scope
@ -18026,7 +18027,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid syntax") assert.Contains(t, rr.Body.String(), util.I18nErrorShareScope)
form.Set("scope", strconv.Itoa(int(share.Scope))) form.Set("scope", strconv.Itoa(int(share.Scope)))
// invalid max tokens // invalid max tokens
form.Set("max_tokens", "t") form.Set("max_tokens", "t")
@ -18037,7 +18038,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid syntax") assert.Contains(t, rr.Body.String(), util.I18nErrorShareMaxTokens)
form.Set("max_tokens", strconv.Itoa(share.MaxTokens)) form.Set("max_tokens", strconv.Itoa(share.MaxTokens))
// no csrf token // no csrf token
req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))
@ -18047,7 +18048,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
form.Set("scope", "100") form.Set("scope", "100")
@ -18058,7 +18059,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: invalid scope") assert.Contains(t, rr.Body.String(), util.I18nErrorShareScope)
form.Set("scope", strconv.Itoa(int(share.Scope))) form.Set("scope", strconv.Itoa(int(share.Scope)))
req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))
@ -18106,7 +18107,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "cannot parse") assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpiration)
form.Set("expiration_date", "") form.Set("expiration_date", "")
form.Set(csrfFormToken, "") form.Set(csrfFormToken, "")
@ -18117,7 +18118,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
form.Set("allowed_ip", "1.1.1") form.Set("allowed_ip", "1.1.1")
@ -18128,7 +18129,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: could not parse allow from entry") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidIPMask)
form.Set("allowed_ip", "") form.Set("allowed_ip", "")
req, err = http.NewRequest(http.MethodPost, webClientSharePath+"/"+share.ShareID, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientSharePath+"/"+share.ShareID, bytes.NewBuffer([]byte(form.Encode())))
@ -18170,7 +18171,7 @@ func TestWebUserShare(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Invalid share list") assert.Contains(t, rr.Body.String(), util.I18nError400Message)
req, err = http.NewRequest(http.MethodGet, webClientSharePath+"?path=%2F&files=%5B\"adir\"%5D", nil) req, err = http.NewRequest(http.MethodGet, webClientSharePath+"?path=%2F&files=%5B\"adir\"%5D", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -18246,8 +18247,8 @@ func TestWebUserShareNoPasswordDisabled(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "You are not authorized to share files/folders without a password") assert.Contains(t, rr.Body.String(), util.I18nErrorShareNoPwd)
form.Set("password", defaultPassword) form.Set("password", defaultPassword)
req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode())))
@ -18287,8 +18288,8 @@ func TestWebUserShareNoPasswordDisabled(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "You are not authorized to share files/folders without a password") assert.Contains(t, rr.Body.String(), util.I18nErrorShareNoPwd)
err = os.RemoveAll(user.GetHomeDir()) err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err) assert.NoError(t, err)
@ -18322,7 +18323,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr) checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
@ -18331,7 +18332,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -18348,7 +18349,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: email") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidEmail)
// invalid public key // invalid public key
form.Set("email", email) form.Set("email", email)
form.Set("public_keys[0][public_key]", "invalid") form.Set("public_keys[0][public_key]", "invalid")
@ -18358,7 +18359,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: could not parse key") assert.Contains(t, rr.Body.String(), util.I18nErrorPubKeyInvalid)
// now remove permissions // now remove permissions
form.Set("public_keys[0][public_key]", testPubKey) form.Set("public_keys[0][public_key]", testPubKey)
form.Del("public_keys[1][public_key]") form.Del("public_keys[1][public_key]")
@ -18376,7 +18377,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth) assert.True(t, user.Filters.AllowAPIKeyAuth)
@ -18397,7 +18398,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth) assert.True(t, user.Filters.AllowAPIKeyAuth)
@ -18418,7 +18419,7 @@ func TestWebUserProfile(t *testing.T) {
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth) assert.True(t, user.Filters.AllowAPIKeyAuth)
@ -24086,7 +24087,7 @@ func TestAdminForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Unable to send confirmation code via email") assert.Contains(t, rr.Body.String(), "Error sending confirmation code via email")
smtpCfg = smtp.Config{} smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir, true) err = smtpCfg.Initialize(configDir, true)
@ -24166,7 +24167,7 @@ func TestUserForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "username is mandatory") assert.Contains(t, rr.Body.String(), util.I18nErrorUsernameRequired)
// user cannot reset the password // user cannot reset the password
form.Set("username", user.Username) form.Set("username", user.Username)
req, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode())))
@ -24175,7 +24176,7 @@ func TestUserForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "you are not allowed to reset your password") assert.Contains(t, rr.Body.String(), util.I18nErrorPwdResetForbidded)
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled} user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
@ -24206,7 +24207,7 @@ func TestUserForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "please set a password") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)
// passwords mismatch // passwords mismatch
form.Set("password", altAdminPassword) form.Set("password", altAdminPassword)
req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))
@ -24215,7 +24216,7 @@ func TestUserForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "The two password fields do not match") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoMatch)
// no code // no code
form.Set("confirm_password", altAdminPassword) form.Set("confirm_password", altAdminPassword)
req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))
@ -24224,7 +24225,7 @@ func TestUserForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "please set a confirmation code") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)
// ok // ok
form.Set("code", lastResetCode) form.Set("code", lastResetCode)
req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode()))) req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode())))
@ -24276,7 +24277,7 @@ func TestUserForgotPassword(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = executeRequest(req) rr = executeRequest(req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Unable to associate the confirmation code with an existing user") assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric)
} }
func TestAPIForgotPassword(t *testing.T) { func TestAPIForgotPassword(t *testing.T) {

View file

@ -25,6 +25,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@ -1215,7 +1216,7 @@ func TestCreateShareCookieError(t *testing.T) {
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
server.handleClientShareLoginPost(rr, req) server.handleClientShareLoginPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), common.ErrInternalFailure.Error()) assert.Contains(t, rr.Body.String(), util.I18nError500Message)
err = dataprovider.DeleteUser(username, "", "", "") err = dataprovider.DeleteUser(username, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)
@ -1316,7 +1317,7 @@ func TestCreateTokenError(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebClientChangePwdPost(rr, req) server.handleWebClientChangePwdPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@ -1349,14 +1350,14 @@ func TestCreateTokenError(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebClientTwoFactorPost(rr, req) server.handleWebClientTwoFactorPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)
req, _ = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath+"?a=a%C3%AO%GD", bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath+"?a=a%C3%AO%GD", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebClientTwoFactorRecoveryPost(rr, req) server.handleWebClientTwoFactorRecoveryPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)
req, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+"?a=a%C3%A1%GD", bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+"?a=a%C3%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@ -1370,7 +1371,7 @@ func TestCreateTokenError(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebClientForgotPwdPost(rr, req) server.handleWebClientForgotPwdPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)
req, _ = http.NewRequest(http.MethodPost, webAdminResetPwdPath+"?a=a%C3%AO%JD", bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webAdminResetPwdPath+"?a=a%C3%AO%JD", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@ -1391,7 +1392,7 @@ func TestCreateTokenError(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebClientPasswordResetPost(rr, req) server.handleWebClientPasswordResetPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm)
req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath+"?a=a%K3%AO%GA", bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath+"?a=a%K3%AO%GA", bytes.NewBuffer([]byte(form.Encode())))
_, err = getShareFromPostFields(req) _, err = getShareFromPostFields(req)
@ -2552,7 +2553,7 @@ func TestChangeUserPwd(t *testing.T) {
} }
err = doChangeUserPassword(req, "a", "b", "b") err = doChangeUserPassword(req, "a", "b", "b")
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "invalid token claims") assert.Contains(t, err.Error(), errInvalidTokenClaims.Error())
} }
} }
@ -2579,70 +2580,70 @@ func TestWebUserInvalidClaims(t *testing.T) {
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientGetFiles(rr, req) server.handleClientGetFiles(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil) req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientGetDirContents(rr, req) server.handleClientGetDirContents(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorDirList403)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil) req, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleWebClientDownloadZip(rr, req) server.handleWebClientDownloadZip(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil) req, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientEditFile(rr, req) server.handleClientEditFile(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil) req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientAddShareGet(rr, req) server.handleClientAddShareGet(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil) req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientUpdateShareGet(rr, req) server.handleClientUpdateShareGet(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil) req, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientAddSharePost(rr, req) server.handleClientAddSharePost(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, webClientSharePath+"/id", nil) req, _ = http.NewRequest(http.MethodPost, webClientSharePath+"/id", nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientUpdateSharePost(rr, req) server.handleClientUpdateSharePost(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil) req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientGetShares(rr, req) server.handleClientGetShares(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, webClientViewPDFPath, nil) req, _ = http.NewRequest(http.MethodGet, webClientViewPDFPath, nil)
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
server.handleClientGetPDF(rr, req) server.handleClientGetPDF(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
} }
func TestInvalidClaims(t *testing.T) { func TestInvalidClaims(t *testing.T) {
@ -3511,6 +3512,34 @@ func TestShareRedirectURL(t *testing.T) {
assert.Empty(t, res) assert.Empty(t, res)
} }
func TestI18NMessages(t *testing.T) {
msg := i18nListDirMsg(http.StatusForbidden)
require.Equal(t, util.I18nErrorDirList403, msg)
msg = i18nListDirMsg(http.StatusInternalServerError)
require.Equal(t, util.I18nErrorDirListGeneric, msg)
msg = i18nFsMsg(http.StatusForbidden)
require.Equal(t, util.I18nError403Message, msg)
msg = i18nFsMsg(http.StatusInternalServerError)
require.Equal(t, util.I18nErrorFsGeneric, msg)
}
func TestI18NErrors(t *testing.T) {
err := util.NewValidationError("error text")
errI18n := util.NewI18nError(err, util.I18nError500Message)
assert.ErrorIs(t, errI18n, util.ErrValidation)
assert.Equal(t, err.Error(), errI18n.Error())
assert.Equal(t, util.I18nError500Message, getI18NErrorString(errI18n, ""))
err2 := util.NewI18nError(fs.ErrNotExist, util.I18nError500Message)
assert.ErrorIs(t, err2, &util.I18nError{})
assert.ErrorIs(t, err2, fs.ErrNotExist)
assert.NotErrorIs(t, err2, fs.ErrExist)
assert.Equal(t, util.I18nError403Message, getI18NErrorString(fs.ErrClosed, util.I18nError403Message))
errorString := getI18NErrorString(nil, util.I18nError500Message)
assert.Equal(t, util.I18nError500Message, errorString)
errI18nWrap := util.NewI18nError(errI18n, util.I18nError404Message)
assert.Equal(t, util.I18nError500Message, errI18nWrap.I18nMessage)
}
func isSharedProviderSupported() bool { func isSharedProviderSupported() bool {
// SQLite shares the implementation with other SQL-based provider but it makes no sense // SQLite shares the implementation with other SQL-based provider but it makes no sense
// to use it outside test cases // to use it outside test cases

View file

@ -204,7 +204,7 @@ func (s *httpdServer) checkHTTPUserPerm(perm string) func(next http.Handler) htt
// for web client perms are negated and not granted // for web client perms are negated and not granted
if tokenClaims.hasPerm(perm) { if tokenClaims.hasPerm(perm) {
if isWebRequest(r) { if isWebRequest(r) {
s.renderClientForbiddenPage(w, r, "You don't have permission for this action") s.renderClientForbiddenPage(w, r, errors.New("you don't have permission for this action"))
} else { } else {
sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden) sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
} }
@ -231,17 +231,24 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
tokenClaims := jwtTokenClaims{} tokenClaims := jwtTokenClaims{}
tokenClaims.Decode(claims) tokenClaims.Decode(claims)
if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword { if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword {
var message string var err error
if tokenClaims.MustSetTwoFactorAuth { if tokenClaims.MustSetTwoFactorAuth {
message = fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v", err = util.NewI18nError(
strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")) util.NewGenericError(
fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))),
util.I18nError2FARequired,
)
} else { } else {
message = "Password change required. Please set a new password to continue to use your account" err = util.NewI18nError(
util.NewGenericError("Password change required. Please set a new password to continue to use your account"),
util.I18nErrorChangePwdRequired,
)
} }
if isWebRequest(r) { if isWebRequest(r) {
s.renderClientForbiddenPage(w, r, message) s.renderClientForbiddenPage(w, r, err)
} else { } else {
sendAPIResponse(w, r, nil, message, http.StatusForbidden) sendAPIResponse(w, r, err, "", http.StatusForbidden)
} }
return return
} }
@ -254,7 +261,10 @@ func (s *httpdServer) requireBuiltinLogin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isLoggedInWithOIDC(r) { if isLoggedInWithOIDC(r) {
if isWebClientRequest(r) { if isWebClientRequest(r) {
s.renderClientForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID") s.renderClientForbiddenPage(w, r, util.NewI18nError(
util.NewGenericError("This feature is not available if you are logged in with OpenID"),
util.I18nErrorNoOIDCFeature,
))
} else { } else {
s.renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID") s.renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
} }

View file

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

View file

@ -148,7 +148,7 @@ func TestOIDCLoginLogout(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
server.router.ServeHTTP(rr, r) server.router.ServeHTTP(rr, r)
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Authentication state did not match") assert.Contains(t, rr.Body.String(), util.I18nInvalidAuth)
expiredAuthReq := oidcPendingAuth{ expiredAuthReq := oidcPendingAuth{
State: xid.New().String(), State: xid.New().String(),
@ -162,7 +162,7 @@ func TestOIDCLoginLogout(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
server.router.ServeHTTP(rr, r) server.router.ServeHTTP(rr, r)
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Authentication state did not match") assert.Contains(t, rr.Body.String(), util.I18nInvalidAuth)
oidcMgr.removePendingAuth(expiredAuthReq.State) oidcMgr.removePendingAuth(expiredAuthReq.State)
server.binding.OIDC.oauth2Config = &mockOAuth2Config{ server.binding.OIDC.oauth2Config = &mockOAuth2Config{

View file

@ -163,8 +163,8 @@ func (s *httpdServer) refreshCookie(next http.Handler) http.Handler {
func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) { func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) {
data := loginPage{ data := loginPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: util.I18nLoginTitle,
CurrentURL: webClientLoginPath, CurrentURL: webClientLoginPath,
Version: version.Get().Version,
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebClient, Branding: s.binding.Branding.WebClient,
@ -198,17 +198,17 @@ func (s *httpdServer) handleWebClientLogout(w http.ResponseWriter, r *http.Reque
func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.renderClientChangePasswordPage(w, r, err.Error()) s.renderClientChangePasswordPage(w, r, util.I18nErrorInvalidForm)
return return
} }
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil { if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
s.renderClientForbiddenPage(w, r, err.Error()) s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
return return
} }
err := doChangeUserPassword(r, strings.TrimSpace(r.Form.Get("current_password")), err := doChangeUserPassword(r, strings.TrimSpace(r.Form.Get("current_password")),
strings.TrimSpace(r.Form.Get("new_password1")), strings.TrimSpace(r.Form.Get("new_password2"))) strings.TrimSpace(r.Form.Get("new_password1")), strings.TrimSpace(r.Form.Get("new_password2")))
if err != nil { if err != nil {
s.renderClientChangePasswordPage(w, r, err.Error()) s.renderClientChangePasswordPage(w, r, getI18NErrorString(err, util.I18nErrorChangePwdGeneric))
return return
} }
s.handleWebClientLogout(w, r) s.handleWebClientLogout(w, r)
@ -228,7 +228,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.renderClientLoginPage(w, r, err.Error(), ipAddr) s.renderClientLoginPage(w, r, util.I18nErrorInvalidForm, ipAddr)
return return
} }
protocol := common.ProtocolHTTP protocol := common.ProtocolHTTP
@ -237,33 +237,33 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
if username == "" || password == "" { if username == "" || password == "" {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials) dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
s.renderClientLoginPage(w, r, "Invalid credentials", ipAddr) s.renderClientLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err) dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientLoginPage(w, r, err.Error(), ipAddr) s.renderClientLoginPage(w, r, util.I18nErrorInvalidCSRF, ipAddr)
return return
} }
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil { if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err) dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientLoginPage(w, r, fmt.Sprintf("access denied: %v", err), ipAddr) s.renderClientLoginPage(w, r, util.I18nError403Message, ipAddr)
return return
} }
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol) user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)
if err != nil { if err != nil {
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err) updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientLoginPage(w, r, dataprovider.ErrInvalidCredentials.Error(), ipAddr) s.renderClientLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String()) connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
if err := checkHTTPClientUser(&user, r, connectionID, true); err != nil { if err := checkHTTPClientUser(&user, r, connectionID, true); err != nil {
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err) updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientLoginPage(w, r, err.Error(), ipAddr) s.renderClientLoginPage(w, r, getI18NErrorString(err, util.I18nError403Message), ipAddr)
return return
} }
@ -272,7 +272,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
if err != nil { if err != nil {
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err) logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure) updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
s.renderClientLoginPage(w, r, err.Error(), ipAddr) s.renderClientLoginPage(w, r, getI18NErrorString(err, util.I18nErrorFsGeneric), ipAddr)
return return
} }
s.loginUser(w, r, &user, connectionID, ipAddr, false, s.renderClientLoginPage) s.loginUser(w, r, &user, connectionID, ipAddr, false, s.renderClientLoginPage)
@ -284,28 +284,28 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
s.renderClientResetPwdPage(w, r, err.Error(), ipAddr) s.renderClientResetPwdPage(w, r, util.I18nErrorInvalidForm, ipAddr)
return return
} }
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderClientForbiddenPage(w, r, err.Error()) s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF))
return return
} }
newPassword := strings.TrimSpace(r.Form.Get("password")) newPassword := strings.TrimSpace(r.Form.Get("password"))
confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password")) confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password"))
if newPassword != confirmPassword { if newPassword != confirmPassword {
s.renderClientResetPwdPage(w, r, "The two password fields do not match", ipAddr) s.renderClientResetPwdPage(w, r, util.I18nErrorChangePwdNoMatch, ipAddr)
return return
} }
_, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")), _, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
newPassword, false) newPassword, false)
if err != nil { if err != nil {
s.renderClientResetPwdPage(w, r, err.Error(), ipAddr) s.renderClientResetPwdPage(w, r, getI18NErrorString(err, util.I18nErrorChangePwdGeneric), ipAddr)
return return
} }
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String()) connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
if err := checkHTTPClientUser(user, r, connectionID, true); err != nil { if err := checkHTTPClientUser(user, r, connectionID, true); err != nil {
s.renderClientResetPwdPage(w, r, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()), ipAddr) s.renderClientResetPwdPage(w, r, getI18NErrorString(err, util.I18nErrorDirList403), ipAddr)
return return
} }
@ -313,7 +313,7 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
err = user.CheckFsRoot(connectionID) err = user.CheckFsRoot(connectionID)
if err != nil { if err != nil {
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err) logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
s.renderClientResetPwdPage(w, r, fmt.Sprintf("Password reset successfully but unable to login: %s", err.Error()), ipAddr) s.renderClientResetPwdPage(w, r, util.I18nErrorLoginAfterReset, ipAddr)
return return
} }
s.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage) s.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage)
@ -328,17 +328,17 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.renderClientTwoFactorRecoveryPage(w, r, err.Error(), ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidForm, ipAddr)
return return
} }
username := claims.Username username := claims.Username
recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code")) recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code"))
if username == "" || recoveryCode == "" { if username == "" || recoveryCode == "" {
s.renderClientTwoFactorRecoveryPage(w, r, "Invalid credentials", ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderClientTwoFactorRecoveryPage(w, r, err.Error(), ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCSRF, ipAddr)
return return
} }
user, userMerged, err := dataprovider.GetUserVariants(username, "") user, userMerged, err := dataprovider.GetUserVariants(username, "")
@ -346,7 +346,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
if errors.Is(err, util.ErrNotFound) { if errors.Is(err, util.ErrNotFound) {
handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
} }
s.renderClientTwoFactorRecoveryPage(w, r, "Invalid credentials", ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
if !userMerged.Filters.TOTPConfig.Enabled || !util.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) { if !userMerged.Filters.TOTPConfig.Enabled || !util.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
@ -360,7 +360,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
} }
if code.Secret.GetPayload() == recoveryCode { if code.Secret.GetPayload() == recoveryCode {
if code.Used { if code.Used {
s.renderClientTwoFactorRecoveryPage(w, r, "This recovery code was already used", ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
user.Filters.RecoveryCodes[idx].Used = true user.Filters.RecoveryCodes[idx].Used = true
@ -377,7 +377,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
} }
} }
handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
s.renderClientTwoFactorRecoveryPage(w, r, "Invalid recovery code", ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
} }
func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) {
@ -389,7 +389,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.renderClientTwoFactorPage(w, r, err.Error(), ipAddr) s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidForm, ipAddr)
return return
} }
username := claims.Username username := claims.Username
@ -397,25 +397,25 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
if username == "" || passcode == "" { if username == "" || passcode == "" {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials) dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
s.renderClientTwoFactorPage(w, r, "Invalid credentials", ipAddr) s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err) dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientTwoFactorPage(w, r, err.Error(), ipAddr) s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCSRF, ipAddr)
return return
} }
user, err := dataprovider.GetUserWithGroupSettings(username, "") user, err := dataprovider.GetUserWithGroupSettings(username, "")
if err != nil { if err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err) dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientTwoFactorPage(w, r, "Invalid credentials", ipAddr) s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) { if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure) updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
s.renderClientTwoFactorPage(w, r, "Two factory authentication is not enabled", ipAddr) s.renderClientTwoFactorPage(w, r, util.I18n2FADisabled, ipAddr)
return return
} }
err = user.Filters.TOTPConfig.Secret.Decrypt() err = user.Filters.TOTPConfig.Secret.Decrypt()
@ -428,7 +428,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
user.Filters.TOTPConfig.Secret.GetPayload()) user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil { if !match || err != nil {
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials) updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials)
s.renderClientTwoFactorPage(w, r, "Invalid authentication code", ipAddr) s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
return return
} }
connectionID := fmt.Sprintf("%s_%s", getProtocolFromRequest(r), xid.New().String()) connectionID := fmt.Sprintf("%s_%s", getProtocolFromRequest(r), xid.New().String())
@ -575,8 +575,8 @@ func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Req
func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) { func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) {
data := loginPage{ data := loginPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: util.I18nLoginTitle,
CurrentURL: webAdminLoginPath, CurrentURL: webAdminLoginPath,
Version: version.Get().Version,
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebAdmin, Branding: s.binding.Branding.WebAdmin,
@ -734,7 +734,7 @@ func (s *httpdServer) loginUser(
if err != nil { if err != nil {
logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err) logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err)
updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure) updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
errorFunc(w, r, err.Error(), ipAddr) errorFunc(w, r, util.I18nError500Message, ipAddr)
return return
} }
if isSecondFactorAuth { if isSecondFactorAuth {
@ -1095,11 +1095,14 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
if err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolHTTP); err != nil { if err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolHTTP); err != nil {
logger.Log(logger.LevelDebug, common.ProtocolHTTP, "", "connection not allowed from ip %q: %v", ipAddr, err) logger.Log(logger.LevelDebug, common.ProtocolHTTP, "", "connection not allowed from ip %q: %v", ipAddr, err)
s.sendForbiddenResponse(w, r, err.Error()) s.sendForbiddenResponse(w, r, util.NewI18nError(err, util.I18nErrorConnectionForbidden))
return return
} }
if common.IsBanned(ipAddr, common.ProtocolHTTP) { if common.IsBanned(ipAddr, common.ProtocolHTTP) {
s.sendForbiddenResponse(w, r, "your IP address is blocked") s.sendForbiddenResponse(w, r, util.NewI18nError(
util.NewGenericError("your IP address is blocked"),
util.I18nErrorIPForbidden),
)
return return
} }
if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil { if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil {
@ -1118,8 +1121,8 @@ func (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http.
if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) { if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
r = s.updateContextFromCookie(r) r = s.updateContextFromCookie(r)
if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) { if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
s.renderClientMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests,
http.StatusTooManyRequests, err, "") util.NewI18nError(errors.New(http.StatusText(http.StatusTooManyRequests)), util.I18nError429Message), "")
return return
} }
s.renderMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", http.StatusTooManyRequests, s.renderMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", http.StatusTooManyRequests,
@ -1129,17 +1132,17 @@ func (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http.
sendAPIResponse(w, r, err, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) sendAPIResponse(w, r, err, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
} }
func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Request, message string) { func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Request, err error) {
if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) { if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
r = s.updateContextFromCookie(r) r = s.updateContextFromCookie(r)
if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) { if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
s.renderClientForbiddenPage(w, r, message) s.renderClientForbiddenPage(w, r, err)
return return
} }
s.renderForbiddenPage(w, r, message) s.renderForbiddenPage(w, r, err.Error())
return return
} }
sendAPIResponse(w, r, errors.New(message), message, http.StatusForbidden) sendAPIResponse(w, r, err, "", http.StatusForbidden)
} }
func (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) {
@ -1150,7 +1153,10 @@ func (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) {
break break
} }
} }
s.sendForbiddenResponse(w, r, fmt.Sprintf("The host %q is not allowed", host)) s.sendForbiddenResponse(w, r, util.NewI18nError(
util.NewGenericError(fmt.Sprintf("The host %q is not allowed", host)),
util.I18nErrorConnectionForbidden,
))
} }
func (s *httpdServer) notFoundHandler(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) notFoundHandler(w http.ResponseWriter, r *http.Request) {

View file

@ -15,10 +15,15 @@
package httpd package httpd
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/unrolled/secure" "github.com/unrolled/secure"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/version"
) )
const ( const (
@ -44,21 +49,26 @@ const (
templateCommonBase = "base.html" templateCommonBase = "base.html"
) )
var (
errInvalidTokenClaims = errors.New("invalid token claims")
)
type commonBasePage struct { type commonBasePage struct {
CSPNonce string CSPNonce string
StaticURL string StaticURL string
Version string
} }
type loginPage struct { type loginPage struct {
commonBasePage commonBasePage
CurrentURL string CurrentURL string
Version string
Error string Error string
CSRFToken string CSRFToken string
AltLoginURL string AltLoginURL string
AltLoginName string AltLoginName string
ForgotPwdURL string ForgotPwdURL string
OpenIDLoginURL string OpenIDLoginURL string
Title string
Branding UIBranding Branding UIBranding
FormDisabled bool FormDisabled bool
} }
@ -66,7 +76,6 @@ type loginPage struct {
type twoFactorPage struct { type twoFactorPage struct {
commonBasePage commonBasePage
CurrentURL string CurrentURL string
Version string
Error string Error string
CSRFToken string CSRFToken string
RecoveryURL string RecoveryURL string
@ -110,8 +119,32 @@ func hasPrefixAndSuffix(key, prefix, suffix string) bool {
} }
func getCommonBasePage(r *http.Request) commonBasePage { func getCommonBasePage(r *http.Request) commonBasePage {
v := version.Get()
return commonBasePage{ return commonBasePage{
CSPNonce: secure.CSPNonce(r.Context()), CSPNonce: secure.CSPNonce(r.Context()),
StaticURL: webStaticFilesPath, StaticURL: webStaticFilesPath,
Version: fmt.Sprintf("v%v-%v", v.Version, v.CommitHash),
} }
} }
func i18nListDirMsg(status int) string {
if status == http.StatusForbidden {
return util.I18nErrorDirList403
}
return util.I18nErrorDirListGeneric
}
func i18nFsMsg(status int) string {
if status == http.StatusForbidden {
return util.I18nError403Message
}
return util.I18nErrorFsGeneric
}
func getI18NErrorString(err error, fallback string) string {
var errI18n *util.I18nError
if errors.As(err, &errI18n) {
return errI18n.I18nMessage
}
return fallback
}

View file

@ -826,7 +826,6 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: pageTwoFactorTitle, Title: pageTwoFactorTitle,
CurrentURL: webAdminTwoFactorPath, CurrentURL: webAdminTwoFactorPath,
Version: version.Get().Version,
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
RecoveryURL: webAdminTwoFactorRecoveryPath, RecoveryURL: webAdminTwoFactorRecoveryPath,
@ -840,7 +839,6 @@ func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: pageTwoFactorRecoveryTitle, Title: pageTwoFactorRecoveryTitle,
CurrentURL: webAdminTwoFactorRecoveryPath, CurrentURL: webAdminTwoFactorRecoveryPath,
Version: version.Get().Version,
Error: error, Error: error,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebAdmin, Branding: s.binding.Branding.WebAdmin,

File diff suppressed because it is too large Load diff

171
internal/util/i18n.go Normal file
View file

@ -0,0 +1,171 @@
// Copyright (C) 2023 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package util
import (
"errors"
)
// localization id for the Web frontend
const (
I18nLoginTitle = "title.login"
I18nShareLoginTitle = "title.share_login"
I18nFilesTitle = "title.files"
I18nSharesTitle = "title.shares"
I18nShareAddTitle = "title.add_share"
I18nShareUpdateTitle = "title.update_share"
I18nProfileTitle = "title.profile"
I18nChangePwdTitle = "title.change_password"
I18n2FATitle = "title.two_factor_auth"
I18nEditFileTitle = "title.edit_file"
I18nViewFileTitle = "title.view_file"
I18nForgotPwdTitle = "title.recovery_password"
I18nResetPwdTitle = "title.reset_password"
I18nSharedFilesTitle = "title.shared_files"
I18nShareUploadTitle = "title.upload_to_share"
I18nShareDownloadTitle = "title.download_shared_file"
I18nShareAccessErrorTitle = "title.share_access_error"
I18nInvalidAuthReqTitle = "title.invalid_auth_request"
I18nError403Title = "title.error403"
I18nError400Title = "title.error400"
I18nError416Title = "title.error416"
I18nError429Title = "title.error429"
I18nError500Title = "title.error500"
I18nErrorPDFTitle = "title.errorPDF"
I18nErrorEditorTitle = "title.error_editor"
I18nInvalidAuth = "general.invalid_auth_request"
I18nError429Message = "general.error429"
I18nError400Message = "general.error400"
I18nError403Message = "general.error403"
I18nError404Message = "general.error404"
I18nError416Message = "general.error416"
I18nError500Message = "general.error500"
I18nErrorPDFMessage = "general.errorPDF"
I18nErrorInvalidToken = "general.invalid_token"
I18nErrorInvalidForm = "general.invalid_form"
I18nErrorInvalidCredentials = "general.invalid_credentials"
I18nErrorInvalidCSRF = "general.invalid_csrf"
I18nErrorFsGeneric = "fs.err_generic"
I18nErrorDirListGeneric = "fs.dir_list.err_generic"
I18nErrorDirList403 = "fs.dir_list.err_403"
I18nErrorDirList429 = "fs.dir_list.err_429"
I18nErrorDirListUser = "fs.dir_list.err_user"
I18nErrorFsValidation = "fs.err_validation"
I18nErrorChangePwdRequiredFields = "change_pwd.required_fields"
I18nErrorChangePwdNoMatch = "change_pwd.no_match"
I18nErrorChangePwdGeneric = "change_pwd.generic"
I18nErrorChangePwdNoDifferent = "change_pwd.no_different"
I18nErrorChangePwdCurrentNoMatch = "change_pwd.current_no_match"
I18nErrorChangePwdRequired = "change_pwd.required"
I18nErrorUsernameRequired = "general.username_required"
I18nErrorGetUser = "general.err_user"
I18nErrorPwdResetForbidded = "login.reset_pwd_forbidden"
I18nErrorPwdResetNoEmail = "login.reset_pwd_no_email"
I18nErrorPwdResetSendEmail = "login.reset_pwd_send_email_err"
I18nErrorPwdResetGeneric = "login.reset_pwd_err_generic"
I18nErrorProtocolForbidden = "general.err_protocol_forbidden"
I18nErrorPwdLoginForbidden = "general.pwd_login_forbidden"
I18nErrorIPForbidden = "general.ip_forbidden"
I18nErrorConnectionForbidden = "general.connection_forbidden"
I18nErrorReservedUsername = "user.username_reserved"
I18nErrorInvalidEmail = "general.email_invalid"
I18nErrorInvalidUser = "user.username_invalid"
I18nErrorHomeRequired = "user.home_required"
I18nErrorHomeInvalid = "user.home_invalid"
I18nErrorPubKeyInvalid = "user.pub_key_invalid"
I18nErrorPrimaryGroup = "user.err_primary_group"
I18nErrorDuplicateGroup = "user.err_duplicate_group"
I18nErrorNoPermission = "user.no_permissions"
I18nErrorNoRootPermission = "user.no_root_permissions"
I18nErrorGenericPermission = "user.err_permissions_generic"
I18nError2FAInvalid = "user.2fa_invalid"
I18nErrorRecoveryCodesInvalid = "user.recovery_codes_invalid"
I18nErrorFolderNameRequired = "general.foldername_required"
I18nErrorFolderMountPathRequired = "user.folder_path_required"
I18nErrorDuplicatedFolders = "user.folder_duplicated"
I18nErrorOverlappedFolders = "user.folder_overlapped"
I18nErrorFolderQuotaSizeInvalid = "user.folder_quota_size_invalid"
I18nErrorFolderQuotaFileInvalid = "user.folder_quota_file_invalid"
I18nErrorFolderQuotaInvalid = "user.folder_quota_invalid"
I18nErrorPasswordComplexity = "general.err_password_complexity"
I18nErrorIPFiltersInvalid = "user.ip_filters_invalid"
I18nErrorSourceBWLimitInvalid = "user.src_bw_limits_invalid"
I18nErrorShareExpirationInvalid = "user.share_expiration_invalid"
I18nErrorFilePatternPathInvalid = "user.file_pattern_path_invalid"
I18nErrorFilePatternDuplicated = "user.file_pattern_duplicated"
I18nErrorFilePatternInvalid = "user.file_pattern_invalid"
I18nErrorDisableActive2FA = "user.disable_active_2fa"
I18nErrorPwdChangeConflict = "user.pwd_change_conflict"
I18nErrorLoginAfterReset = "login.reset_ok_login_error"
I18nErrorShareScope = "share.scope_invalid"
I18nErrorShareMaxTokens = "share.max_tokens_invalid"
I18nErrorShareExpiration = "share.expiration_invalid"
I18nErrorShareNoPwd = "share.err_no_password"
I18nErrorShareExpirationOutOfRange = "share.expiration_out_of_range"
I18nErrorShareGeneric = "share.generic"
I18nErrorNameRequired = "general.name_required"
I18nErrorSharePathRequired = "share.path_required"
I18nErrorShareWriteScope = "share.path_write_scope"
I18nErrorShareNestedPaths = "share.nested_paths"
I18nErrorShareExpirationPast = "share.expiration_past"
I18nErrorInvalidIPMask = "general.allowed_ip_mask_invalid"
I18nErrorShareUsage = "share.usage_exceed"
I18nErrorShareExpired = "share.expired"
I18nErrorLoginFromIPDenied = "login.ip_not_allowed"
I18nError2FARequired = "login.two_factor_required"
I18nErrorNoOIDCFeature = "general.no_oidc_feature"
I18nErrorNoPermissions = "general.no_permissions"
I18nErrorShareBrowsePaths = "share.browsable_multiple_paths"
I18nErrorShareBrowseNoDir = "share.browsable_non_dir"
I18nErrorPathInvalid = "general.path_invalid"
I18nErrorQuotaRead = "general.err_quota_read"
I18nErrorEditDir = "general.error_edit_dir"
I18nErrorEditSize = "general.error_edit_size"
I18nProfileUpdated = "general.profile_updated"
I18nShareLoginOK = "general.share_ok"
I18n2FADisabled = "2fa.disabled"
)
// NewI18nError returns a I18nError wrappring the provided error
func NewI18nError(err error, message string) *I18nError {
var errI18n *I18nError
if errors.As(err, &errI18n) {
return errI18n
}
return &I18nError{
err: err,
I18nMessage: message,
}
}
// I18nError is an error wrapper that add a message to use for localization.
type I18nError struct {
err error
I18nMessage string
}
// Error returns the wrapped error string.
func (e *I18nError) Error() string {
return e.err.Error()
}
// Is reports if target matches
func (e *I18nError) Is(target error) bool {
if errors.Is(e.err, target) {
return true
}
_, ok := target.(*I18nError)
return ok
}

View file

@ -343,7 +343,7 @@ func GetIntFromPointer(val *int64) int64 {
// GetTimeFromPointer returns the time value or now // GetTimeFromPointer returns the time value or now
func GetTimeFromPointer(val *time.Time) time.Time { func GetTimeFromPointer(val *time.Time) time.Time {
if val == nil { if val == nil {
return time.Now() return time.Unix(0, 0)
} }
return *val return *val
} }

View file

@ -0,0 +1,370 @@
{
"title": {
"login": "Login",
"share_login": "Share Login",
"profile": "Profile",
"change_password": "Change password",
"files": "Files",
"shares": "Shares",
"add_share": "Add share",
"update_share": "Update share",
"two_factor_auth": "Two-factor authentication",
"two_factor_auth_short": "2FA",
"edit_file": "Edit file",
"view_file": "View file",
"recovery_password": "Password recovery",
"reset_password": "Password reset",
"shared_files": "Shared files",
"upload_to_share": "Upload to share",
"download_shared_file": "Download shared file",
"share_access_error": "Unable to access the share",
"invalid_auth_request": "Invalid authentication request",
"error429": "Too Many Requests",
"error403": "Forbidden",
"error400": "Bad Request",
"error416": "Requested Range Not Satisfiable",
"error500": "Internal Server Error",
"errorPDF": "Unable to show PDF file",
"error_editor": "Cannot open file editor"
},
"login": {
"username": "Username",
"password": "Password",
"your_username": "Your username",
"forgot_password": "Forgot Password?",
"forgot_password_msg": "Enter your account username below, you will receive a password reset code by email.",
"send_reset_code": "Send Reset Code",
"signin": "Sign in",
"signin_openid": "Sign in with OpenID",
"signout": "Sign out",
"auth_code": "Authentication code",
"two_factor_help": "Open the two-factor authentication app on your device to view your authentication code and verify your identity.",
"two_factor_msg": "Enter a two-factor recovery code",
"recovery_code": "Recovery code",
"recovery_code_msg": "You can enter one of your recovery codes in case you lost access to your mobile device.",
"reset_password": "Reset Password",
"reset_pwd_msg": "Check your email for the confirmation code",
"reset_submit": "Update Password and Sign in",
"confirm_code": "Confirmation code",
"reset_pwd_forbidden": "You are not allowed to reset your password",
"reset_pwd_no_email": "Your account does not have an email address, it is not possible to reset your password by sending an email verification code",
"reset_pwd_send_email_err": "Unable to send confirmation code via email",
"reset_pwd_err_generic": "Unexpected error while resetting password",
"reset_ok_login_error": "The password reset completed successfully but an unexpected error occurred while signing in",
"ip_not_allowed": "Login is not allowed from this IP address",
"two_factor_required": "Two-factor authentication is required, set it up"
},
"theme": {
"light": "Light",
"dark": "Dark",
"system": "Auto"
},
"general": {
"invalid_auth_request": "The authentication request does not meet security requirements",
"error400": "The received request is not valid",
"error403": "You do not have the required permissions",
"error404": "The requested resource does not exist",
"error416": "The requested file fragment could not be returned",
"error429": "Rate limit exceeded",
"error500": "The server is unable to fulfill your request",
"errorPDF": "This file does not look like a PDF",
"error_edit_dir": "Cannot edit a directory",
"error_edit_size": "The file size exceeds the maximum allowed size",
"invalid_form": "Invalid form",
"invalid_credentials": "Invalid credentials",
"invalid_csrf": "The form token is not valid",
"invalid_token": "Invalid permissions",
"confirm_logout": "Are you sure you want to sign out?",
"wait": "Please wait...",
"or": "or",
"ok": "OK",
"none": "None",
"cancel": "Cancel",
"submit": "Save",
"back": "Back",
"add": "Add",
"enable": "Enable",
"disable": "Disable",
"close": "Close",
"search": "Search",
"configuration": "Configuration",
"config_saved": "Configuration saved",
"rename": "Rename",
"confirm": "Confirm",
"edit": "Edit",
"delete": "Delete",
"delete_confirm": "Do you want to delete \"{{- name}}\"? This action is irreversible",
"delete_confirm_generic": "Do you want to delete the selected item? This action is irreversible",
"delete_multi_confirm": "Do you want to delete the selected items? This action is irreversible",
"delete_error_generic": "Unable to delete the selected item",
"delete_error_403": "$t(general.delete_error_generic). $t(general.error403)",
"delete_error_404": "$t(general.delete_error_generic). $t(general.error404)",
"loading": "Loading...",
"name": "Name",
"size": "Size",
"last_modified": "Last Modified",
"info": "Info",
"datetime": "{{- val, datetime}}",
"selected_items_one": "{{count}} item selected",
"selected_items_other": "{{count}} items selected",
"name_required": "Name is required",
"name_different": "The new name must be different from the current name",
"html5_media_not_supported": "Your browser does not support HTML5 audio/video.",
"choose_target_folder": "Choose target folder",
"source_name": "Source name",
"target_folder": "Target folder",
"dest_name": "Destination name",
"my_profile": "My profile",
"description": "Description",
"email": "Email",
"api_key_auth": "API key authentication",
"api_key_auth_help": "Allow to impersonate yourself, in REST API, using an API key",
"pub_keys": "Public keys",
"pub_key_placeholder": "Paste your public key here",
"verify": "Verify",
"problems": "Having problems?",
"generate": "Generate",
"view": "View",
"allowed_ip_mask": "Allowed IP/Mask",
"allowed_ip_mask_help": "Comma separated IP/Mask in CIDR format, for example \"192.168.1.0/24,10.8.0.100/32\"",
"allowed_ip_mask_invalid": "Invalid allowed IP/Mask",
"username_required": "The username is required",
"foldername_required": "The folder name is required",
"err_user": "Unable to validate your user",
"err_protocol_forbidden": "HTTP protocol is not allowed for your user",
"pwd_login_forbidden": "The password login method is not allowed for your user",
"ip_forbidden": "Login not allowed from this IP address",
"email_invalid": "The email address is invalid",
"err_password_complexity": "The password provided does not meet the complexity requirements",
"no_oidc_feature": "This feature is not available if you are logged in with OpenID",
"connection_forbidden": "You are not allowed to connect",
"no_permissions": "You are not allowed to change anything",
"path_invalid": "Invalid path",
"err_quota_read": "Read denied due to quota limit",
"profile_updated": "Your profile has been successfully updated",
"share_ok": "Share access successful, you can now use your link",
"qr_code": "QR Code"
},
"fs": {
"view_file": "View file \"{{- path}}\"",
"edit_file": "Edit file \"{{- path}}\"",
"new_folder": "New Folder",
"select_across_pages": "Select across pages",
"actions": "Actions",
"download": "Download",
"download_ready": "Your download is ready",
"move_copy": "Move or copy",
"share": "Share",
"home": "Home",
"create_folder_msg": "Folder name",
"no_more_subfolders": "No more subfolders in here",
"no_files_folders": "No files or folders",
"invalid_name": "File or folder names cannot contain \"/\"",
"folder_name_required": "Folder name is required",
"deleting": "Delete {{idx}}/{{total}}: {{- name}}",
"copying": "Copy {{idx}}/{{total}}: {{- name}}",
"moving": "Move {{idx}}/{{total}}: {{- name}}",
"uploading": "Upload {{idx}}/{{total}}: {{- name}}",
"err_403": "Permission denied",
"err_429": "Too many concurrent requests",
"err_generic": "Unable to access the requested resource",
"err_validation": "Invalid filesystem configuration",
"dir_list": {
"err_generic": "Failed to get directory listing",
"err_403": "$t(fs.dir_list.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.dir_list.err_generic). $t(fs.err_429)",
"err_user": "$t(fs.dir_list.err_generic). $t(general.err_user)"
},
"create_dir": {
"err_generic": "Error creating new folder",
"err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)"
},
"save": {
"err_generic": "Error saving file",
"err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)"
},
"delete": {
"err_generic": "Unable to delete \"{{- name}}\"",
"err_403": "$t(fs.delete.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.delete.err_generic). $t(fs.err_429)"
},
"delete_multi": {
"err_generic_partial": "Not all the selected items have been deleted, please reload the page",
"err_generic": "Unable to delete the selected items",
"err_403_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)",
"err_429_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)",
"err_403": "$t(fs.delete_multi.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.delete_multi.err_generic). $t(fs.err_429)"
},
"copy": {
"msg": "Copy",
"err_generic": "Error copying files/directories",
"err_403": "$t(fs.copy.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.copy.err_generic). $t(fs.err_429)"
},
"move": {
"msg": "Move",
"err_generic": "Error moving files/directories",
"err_403": "$t(fs.move.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.move.err_generic). $t(fs.err_429)"
},
"rename": {
"new_name": "New name",
"err_generic": "Unable to rename \"{{- name}}\"",
"err_403": "$t(fs.rename.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.rename.err_generic). $t(fs.err_429)"
},
"upload": {
"text": "Upload Files",
"success": "Files uploaded successfully",
"message": "Drop files here or click to upload.",
"err_generic": "Error uploading files",
"err_403": "$t(fs.upload.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.upload.err_generic). $t(fs.err_429)"
},
"quota_usage": {
"title": "Quota usage",
"disk": "Disk quota",
"size": "Size: {{- val}}",
"size_percentage": "Size: {{- val}} ({{percentage}}%)",
"files": "Files: {{- num}}",
"files_percentage": "Files: {{- val}} ({{percentage}}%)",
"transfer": "Transfer quota",
"total": "Total: {{- val}}",
"total_percentage": "Total: {{- val}} ({{percentage}}%)",
"uploads": "Uploads: {{- val}}",
"uploads_percentage": "Uploads: {{- val}} ({{percentage}}%)",
"downloads": "Downloads: {{- val}}",
"downloads_percentage": "Downloads: {{- val}} ({{percentage}}%)"
}
},
"datatable": {
"info": "Showing _START_ to _END_ of _TOTAL_ records",
"info_empty": "Showing no record",
"info_filtered": "(filtered from _MAX_ total records)",
"processing": "Processing..."
},
"editor": {
"keybinding": "Editor keybindings",
"search": "Open search panel",
"goto": "Jump to line",
"indent_more": "Indent more",
"indent_less": "Indent less"
},
"2fa": {
"title": "Two-factor authentication using Authenticator apps",
"msg_enabled": "Two-factor authentication is enabled",
"msg_disabled": "Secure Your Account",
"msg_info": "Two-factor authentication adds an extra layer of security to your account. To log in you'll need to provide an additional authentication code.",
"require_for": "Require 2FA for",
"generate": "Generate new secret",
"recovery_codes": "Recovery codes",
"new_recovery_codes": "New recovery codes",
"recovery_codes_msg1": "Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor auth configuration.",
"recovery_codes_msg2": "To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.",
"recovery_codes_msg3": "If you generate new recovery codes, you automatically invalidate old ones.",
"info_title": "Learn about two-factor authentication",
"info1": "SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive authentication.",
"info2": "HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API you have to add the passcode using an HTTP header.",
"info3": "FTP has no standard way to support two-factor authentication, if you enable the FTP support, you have to add the TOTP passcode after the password. For example if your password is \"password\" and your one time passcode is \"123456\" you have to use \"password123456\" as password.",
"info4": "WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.",
"setup_title": "Set up two-factor authentication",
"setup_msg": "Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.",
"setup_help": "If you have trouble using the QR code, select manual entry on your app, and enter the code:",
"disable_question": "Do you want to disable two-factor authentication?",
"generate_question": "Do you want to generate a new secret and invalidate the previous one? Any registered Authenticator App will stop working",
"disabled": "Two-factor authentication is disabled",
"recovery_codes_gen_err": "Failed to generate new recovery codes",
"recovery_codes_get_err": "Unable to obtain recovery codes",
"auth_code_invalid": "Failed to validate the provided authentication code",
"auth_secret_gen_err": "Failed to generate authentication secret",
"save_err": "Failed to save configuration",
"save_err_proto": "$t(2fa.save_err). Make sure the protocols enabled comply with company policy",
"auth_code_required": "The authentication code is required",
"no_protocol": "Please select at least a protocol"
},
"share": {
"scope": "Scope",
"scope_read": "Read",
"scope_write": "Write",
"scope_read_write": "Read/Write",
"scope_help": "For scope \"Write\" and \"Read/Write\" you have to define a single path and it must be a directory",
"paths": "Paths",
"path_help": "file or directory path, i.e. /dir or /dir/file.txt",
"password_help": "If set the share will be password-protected",
"expiration": "Expiration",
"expiration_help": "Pick an expiration date",
"max_tokens": "Max tokens",
"max_tokens_help": "Maximum number of times this share can be accessed. 0 means no limit",
"view_manage": "View and manage shares",
"no_share": "No sharing",
"password_protected": "Password protected.",
"expiration_date": "Expiration: {{- val, datetime}}. ",
"last_use": "Last use: {{- val, datetime}}. ",
"usage": "Usage: {{used}}/{{total}}. ",
"used_tokens": "Used token: {{used}}. ",
"scope_invalid": "Invalid scope",
"max_tokens_invalid": "Invalid max tokens",
"expiration_invalid": "Invalid expiration",
"err_no_password": "You are not allowed to share files/folders without password",
"expiration_out_of_range": "Set an expiration date and ensure it complies with company policy, e.g. is not too far in the future",
"generic": "Unexpected error saving share",
"path_required": "At least a path is required",
"path_write_scope": "The write scope requires exactly one path",
"nested_paths": "Paths cannot be nested",
"expiration_past": "The expiration date must be in the future",
"usage_exceed": "Maximum sharing usage exceeded",
"expired": "Sharing has expired",
"browsable_multiple_paths": "A share with multiple paths is not browsable",
"browsable_non_dir": "The share is not a directory so it is not browsable"
},
"select2": {
"no_results": "No results found",
"searching": "Searching...",
"removeall": "Remove all items",
"remove": "Remove item"
},
"change_pwd": {
"info": "Enter your current password, for security reasons, and then your new password twice, to verify that you have written it correctly. You will be logged out after changing your password",
"current": "Current password",
"new": "New password",
"confirm": "Confirm password",
"save": "Change my password",
"required_fields": "Please provide the current password and the new one two times",
"no_match": "The two password fields do not match",
"no_different": "The new password must be different from the current one",
"current_no_match": "Current password does not match",
"generic": "Unexpected error while changing password",
"required": "Password change is required. Set a new password to continue using your account"
},
"user": {
"username_reserved": "The specified username is reserved",
"username_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
"home_required": "The home directory is mandatory",
"home_invalid": "The home directory must be an absolute path",
"pub_key_invalid": "Invalid public key",
"err_primary_group": "Only one primary group is allowed",
"err_duplicate_group": "Duplicate groups detected",
"no_permissions": "Directories permissions are mandatory",
"no_root_permissions": "Home directory permissions are required",
"err_permissions_generic": "Invalid permissions: Make sure you use valid absolute paths",
"2fa_invalid": "Invalid configuration for two-factor authentication",
"recovery_codes_invalid": "Invalid recovery codes",
"folder_path_required": "The virtual folder mount path is required",
"folder_duplicated": "Duplicated virtual folders detected",
"folder_overlapped": "Overlapping virtual folders detected",
"folder_quota_size_invalid": "The quota as size of virtual folders must be greater than or equal to -1",
"folder_quota_file_invalid": "The quota as files of virtual folders must be greater than or equal to -1",
"folder_quota_invalid": "Quotas as size and as number of files of virtual folders must be both -1 or greater or equal than zero",
"ip_filters_invalid": "Invalid IP filters, make sure they respect CIDR notation, for example 192.168.1.0/24",
"src_bw_limits_invalid": "Invalid per-source bandwidth speed limits",
"share_expiration_invalid": "The expiration for shares must be greater than or equal to the defined default value",
"file_pattern_path_invalid": "File name pattern filter paths must be absolute",
"file_pattern_duplicated": "Duplicated file name pattern filters detected",
"file_pattern_invalid": "Invalid file name pattern filters",
"disable_active_2fa": "Two-factor authentication cannot be disabled for a user with an active configuration",
"pwd_change_conflict": "It is not possible to request a password change and at the same time prevent the password from being changed"
}
}

View file

@ -0,0 +1,370 @@
{
"title": {
"login": "Accedi",
"share_login": "Accedi alla condivisione",
"profile": "Profilo",
"change_password": "Cambio password",
"files": "File",
"shares": "Condivisioni",
"add_share": "Aggiungi condivisione",
"update_share": "Modifica condivisione",
"two_factor_auth": "Autenticazione a due fattori",
"two_factor_auth_short": "2FA",
"edit_file": "Modifica file",
"view_file": "Visualizza file",
"recovery_password": "Recupero password",
"reset_password": "Reimpostazione password",
"shared_files": "File condivisi",
"upload_to_share": "Carica nella condivisione",
"download_shared_file": "Scarica file condiviso",
"share_access_error": "Impossibile accedere alla condivisione",
"invalid_auth_request": "Richiesta di autenticazione non valida",
"error429": "Troppe richieste",
"error403": "Non permesso",
"error400": "Richiesta non valida",
"error416": "Impossibile tornare l'intervallo richiesto",
"error500": "Errore interno del server",
"errorPDF": "Impossibile mostrare il file PDF",
"error_editor": "Impossibile aprire l'editor di file"
},
"login": {
"username": "Nome utente",
"your_username": "Il tuo nome utente",
"password": "Password",
"forgot_password": "Password dimenticata?",
"forgot_password_msg": "Inserisci il nome utente del tuo account qui sotto, riceverai un codice di reimpostazione della password via e-mail.",
"send_reset_code": "Invia codice di ripristino",
"signin": "Accedi",
"signin_openid": "Accedi con OpenID",
"signout": "Esci",
"auth_code": "Codice di autenticazione",
"two_factor_help": "Apri l'app di autenticazione a due fattori sul tuo dispositivo per visualizzare il tuo codice di autenticazione e verificare la tua identità.",
"two_factor_msg": "Inserisci un codice di ripristino per l'autenticazione a due fattori",
"recovery_code": "Codice di ripristino",
"recovery_code_msg": "Puoi inserire uno dei tuoi codici di ripristino nel caso in cui hai perso l'accesso al tuo dispositivo mobile.",
"reset_password": "Reimposta password",
"reset_pwd_msg": "Controlla la tua email per il codice di conferma",
"reset_submit": "Aggiorna password e accedi",
"confirm_code": "Codice di conferma",
"reset_pwd_forbidden": "Non ti è consentito reimpostare la password",
"reset_pwd_no_email": "Il tuo account non ha un indirizzo email, non è possibile reimpostare la password inviando un codice di verifica via email",
"reset_pwd_send_email_err": "Errore nell'invio del codice di conferma via e-mail",
"reset_pwd_err_generic": "Errore imprevisto durante la reimpostazione della password",
"reset_ok_login_error": "La reimpostazione della password è stata completata correttamente ma si è verificato un errore imprevisto durante l'accesso",
"ip_not_allowed": "L'accesso non è consentito da questo indirizzo IP",
"two_factor_required": "È richiesta l'autenticazione a due fattori, configurala"
},
"theme": {
"light": "Chiaro",
"dark": "Scuro",
"system": "Auto"
},
"general": {
"invalid_auth_request": "La richiesta di autenticazione non soddisfa i requisiti di sicurezza",
"error400": "La richiesta ricevuta non è valida",
"error403": "Non si dispone delle autorizzazioni richieste",
"error404": "La risorsa richiesta non esiste",
"error416": "Il frammento di file richiesto non può essere restituito",
"error429": "Limite di richieste per unità di tempo superato",
"error500": "Il server non è in grado di soddisfare la tua richiesta",
"errorPDF": "Questo file non sembra un PDF",
"error_edit_dir": "Impossibile modificare una cartella",
"error_edit_size": "La dimensione del file supera la dimensione massima consentita",
"invalid_form": "Modulo non valido",
"invalid_credentials": "Credenziali non valide",
"invalid_csrf": "Il token del modulo non è valido",
"invalid_token": "Permessi non validi",
"confirm_logout": "Sei sicuro di volerti disconnettere?",
"wait": "Attendere prego...",
"or": "o",
"ok": "OK",
"none": "Nessuna",
"cancel": "Annulla",
"submit": "Salva",
"back": "Indietro",
"add": "Aggiungi",
"enable": "Abilita",
"disable": "Disabilita",
"close": "Chiudi",
"search": "Cerca",
"configuration": "Configurazione",
"config_saved": "Configurazione salvata",
"rename": "Rinomina",
"confirm": "Conferma",
"edit": "Modifica",
"delete": "Elimina",
"delete_confirm": "Vuoi eliminare \"{{- name}}\"? Questa azione è irreversibile",
"delete_confirm_generic": "Vuoi eliminare l'elemento selezionato? Questa azione è irreversibile",
"delete_multi_confirm": "Vuoi eliminare gli elementi selezionati? Questa azione è irreversibile",
"delete_error_generic": "Impossibile eliminare l'elemento selezionato",
"delete_error_403": "$t(general.delete_error_generic). $t(general.error403)",
"delete_error_404": "$t(general.delete_error_generic). $t(general.error404)",
"loading": "Caricamento...",
"name": "Nome",
"size": "Dimensione",
"last_modified": "Ultima modifica",
"info": "Info",
"datetime": "{{- val, datetime}}",
"selected_items_one": "{{count}} elemento selezionato",
"selected_items_other": "{{count}} elementi selezionati",
"name_required": "Il nome è obbligatorio",
"name_different": "Il nuovo nome deve essere diverso dal nome attuale",
"html5_media_not_supported": "Il tuo browser non supporta audio/video HTML5.",
"choose_target_folder": "Scegli la cartella di destinazione",
"source_name": "Nome di origine",
"target_folder": "Cartella di destinazione",
"dest_name": "Nome di destinazione",
"my_profile": "Il mio profilo",
"description": "Descrizione",
"email": "Email",
"api_key_auth": "Autenticazione mediante chiave API",
"api_key_auth_help": "Permette di imperosare te stesso, nelle API REST, mediante una chiave API",
"pub_keys": "Chiavi pubbliche",
"pub_key_placeholder": "Incolla qui la tua chiave pubblica",
"verify": "Verifica",
"problems": "Hai problemi?",
"generate": "Genera",
"view": "Visualizza",
"allowed_ip_mask": "IP/Reti permesse",
"allowed_ip_mask_help": "IP/reti separate da virgola in formato CIDR, ad esempio \"192.168.1.0/24,10.8.0.100/32\"",
"allowed_ip_mask_invalid": "IP/reti permesse non valide",
"username_required": "Il nome utente è obbligatorio",
"foldername_required": "Il nome della cartella è obbligatorio",
"err_user": "Errore validazione utente",
"err_protocol_forbidden": "Il protocollo HTTP non è consentito per il tuo utente",
"pwd_login_forbidden": "Il metodo di accesso tramite password non è consentito per il tuo utente",
"ip_forbidden": "Accesso non consentito da questo indirizzo IP",
"email_invalid": "L'indirizzo e-mail non è valido",
"err_password_complexity": "La password fornita non soddisfa i requisiti di complessità",
"no_oidc_feature": "Questa funzionalità non è disponibile se hai effettuato l'accesso con OpenID",
"connection_forbidden": "Non ti è consentito connetterti",
"no_permissions": "Non ti è consentito cambiare nulla",
"path_invalid": "Percorso non valido",
"err_quota_read": "Lettura negata a causa del limite di quota",
"profile_updated": "Il tuo profilo è stato aggiornato con successo",
"share_ok": "Accesso alla condivisione riuscito, ora puoi utilizzare il tuo collegamento",
"qr_code": "Codice QR"
},
"fs": {
"view_file": "Visualizza file \"{{- path}}\"",
"edit_file": "Modifica file \"{{- path}}\"",
"new_folder": "Nuova cartella",
"select_across_pages": "Seleziona tra le pagine",
"actions": "Azioni",
"download": "Scarica",
"download_ready": "Il tuo download è pronto",
"move_copy": "Sposta o copia",
"share": "Condividi",
"home": "Home",
"create_folder_msg": "Nome cartella",
"no_more_subfolders": "Non ci sono più sottocartelle qui",
"no_files_folders": "Nessun file o cartella",
"invalid_name": "I nomi di file o cartelle non possono contenere \"/\"",
"folder_name_required": "Il nome della cartella è obbligatorio",
"deleting": "Eliminazione {{idx}}/{{total}}: {{- name}}",
"copying": "Copia {{idx}}/{{total}}: {{- name}}",
"moving": "Spostamento {{idx}}/{{total}}: {{- name}}",
"uploading": "Caricamento {{idx}}/{{total}}: {{- name}}",
"err_403": "Non si dispone delle autorizzazioni richieste",
"err_429": "Troppe richieste contemporanee",
"err_generic": "Impossibile accedere alla risorsa richiesta",
"err_validation": "Configurazione del filesystem non valida",
"dir_list": {
"err_generic": "Impossibile ottenere l'elenco della directory",
"err_403": "$t(fs.dir_list.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.dir_list.err_generic). $t(fs.err_429)",
"err_user": "$t(fs.dir_list.err_generic). $t(general.err_user)"
},
"create_dir": {
"err_generic": "Errore durante la creazione delle nuova cartella",
"err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)"
},
"save": {
"err_generic": "Errore durante il salvataggio del file",
"err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)"
},
"delete": {
"err_generic": "Impossibile eliminare \"{{- name}}\"",
"err_403": "$t(fs.delete.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.delete.err_generic). $t(fs.err_429)"
},
"delete_multi": {
"err_generic_partial": "Non tutti gli elementi selezionati sono stati eliminati, ricarica la pagina",
"err_generic": "Impossibile eliminare gli elementi selezionati",
"err_403_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)",
"err_429_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)",
"err_403": "$t(fs.delete_multi.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.delete_multi.err_generic). $t(fs.err_429)"
},
"copy": {
"msg": "Copia",
"err_generic": "Errore copia file/cartelle",
"err_403": "$t(fs.copy.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.copy.err_generic). $t(fs.err_429)"
},
"move": {
"msg": "Sposta",
"err_generic": "Errore nello spostamento di file/directory",
"err_403": "$t(fs.move.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.move.err_generic). $t(fs.err_429)"
},
"rename": {
"new_name": "Nuovo nome",
"err_generic": "Impossibile rinominare \"{{- name}}\"",
"err_403": "$t(fs.rename.err_generic): $t(fs.err_403)",
"err_429": "$t(fs.rename.err_generic): $t(fs.err_429)"
},
"upload": {
"text": "Carica file",
"success": "File caricati correttamente",
"message": "Rilascia i file qui o fai clic per caricarli.",
"err_generic": "Errore caricamento file",
"err_403": "$t(fs.upload.err_generic). $t(fs.err_403)",
"err_429": "$t(fs.upload.err_generic). $t(fs.err_429)"
},
"quota_usage": {
"title": "Utilizzo quota",
"disk": "Quota disco",
"size": "Dimensione: {{- val}}",
"size_percentage": "Dimensione: {{- val}} ({{percentage}}%)",
"files": "File: {{- val}}",
"files_percentage": "File: {{- val}} ({{percentage}}%)",
"transfer": "Quota trasferimenti",
"total": "Totale: {{- val}}",
"total_percentage": "Totale: {{- val}} ({{percentage}}%)",
"uploads": "Caricamenti: {{- val}}",
"uploads_percentage": "Caricamenti: {{- val}} ({{percentage}}%)",
"downloads": "Download: {{- val}}",
"downloads_percentage": "Download: {{- val}} ({{percentage}}%)"
}
},
"datatable": {
"info": "Risultati da _START_ a _END_ di _TOTAL_ elementi",
"info_empty": "Nessun risultato",
"info_filtered": "(filtrati da _MAX_ elementi totali)",
"processing": "Elaborazione..."
},
"editor": {
"keybinding": "Combinazioni di tasti dell'editor",
"search": "Apre pannello di ricerca",
"goto": "Vai a linea",
"indent_more": "Aumenta indentazione",
"indent_less": "Diminuisci indentazione"
},
"2fa": {
"title": "Autenticazione a due fattori utilizzando App di autenticazione",
"msg_enabled": "L'autenticazione a due fattori è abilitata",
"msg_disabled": "Metti il tuo account in sicurezza",
"msg_info": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account. Per accedere dovrai fornire un ulteriore codice di autenticazione.",
"require_for": "Richiedi 2FA per",
"generate": "Genera nuovo segreto",
"recovery_codes": "Codici di ripristino",
"new_recovery_codes": "Nuovi codici di ripristino",
"recovery_codes_msg1": "I codici di ripristino sono un insieme di codici monouso che possono essere utilizzati al posto del codice di autenticazione per accedere all'interfaccia utente Web. Puoi utilizzarli se perdi l'accesso al tuo telefono per accedere al tuo account e disabilitare o rigenerare la configurazione dell'autenticazione a due fattori.",
"recovery_codes_msg2": "Per mantenere sicuro il tuo account, non condividere o distribuire i tuoi codici di ripristino. Ti consigliamo di salvarli con un gestore di password sicuro.",
"recovery_codes_msg3": "Se generi nuovi codici di ripristino, invalidi automaticamente quelli vecchi.",
"info_title": "Ulteriori informazioni sull'autenticazione a due fattori",
"info1": "Il protocollo SSH (comandi SFTP/SCP/SSH) richiederà il passcode se il client utilizza l'autenticazione interattiva da tastiera.",
"info2": "Per protocollo HTTP si intende l'interfaccia utente Web e le API REST. L'interfaccia utente Web richiederà il passcode utilizzando una pagina specifica. Per l'API REST devi aggiungere il passcode utilizzando un'intestazione HTTP.",
"info3": "FTP non ha un modo standard per supportare l'autenticazione a due fattori, se abiliti il supporto FTP, devi aggiungere il passcode TOTP dopo la password. Ad esempio, se la tua password è \"password\" e il tuo passcode monouso è \"123456\", devi utilizzare \"password123456\" come password.",
"info4": "WebDAV non è supportato poiché ogni singola richiesta deve essere autenticata e un passcode non può essere riutilizzato.",
"setup_title": "Configura l'autenticazione a due fattori",
"setup_msg": "Utilizza la tua app di autenticazione preferita (ad esempio Microsoft Authenticator, Google Authenticator, Authy, 1Password ecc.) per scansionare il codice QR. Verrà generato un codice di autenticazione da inserire di seguito.",
"setup_help": "Se hai problemi con l'utilizzo del codice QR, seleziona l'inserimento manuale sulla tua app e inserisci il codice:",
"disable_question": "Vuoi disattivare l'autenticazione a due fattori?",
"generate_question": "Vuoi generare un nuovo segreto e invalidare quello precedente? Qualsiasi app di autenticazione registrata smetterà di funzionare",
"disabled": "L'autenticazione a due fattori è disabilitata",
"recovery_codes_gen_err": "Impossibile generare nuovi codici di ripristino",
"recovery_codes_get_err": "Impossibile ottenere i codici di ripristino",
"auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito",
"auth_secret_gen_err": "Impossibile generare il segreto di autenticazione",
"save_err": "Impossibile salvare la configurazione",
"save_err_proto": "$t(2fa.save_err). Assicurati che i protocolli abilitati siano conformi alla politica aziendale",
"auth_code_required": "Il codice di autenticazione è obbligatorio",
"no_protocol": "Seleziona almeno un protocollo"
},
"share": {
"scope": "Ambito",
"scope_read": "Lettura",
"scope_write": "Scrittura",
"scope_read_write": "Lettura/Scrittura",
"scope_help": "Per gli ambiti \"Scrittura\" e \"Lettura/Scrittura\" devi definire un singolo percorso e deve essere una cartella",
"paths": "Percorsi",
"path_help": "percorso di un file o di una directory, ad esempio /dir o /dir/file.txt",
"password_help": "Se impostata, la condivisione sarà protetta da password",
"expiration": "Scadenza",
"expiration_help": "Scegli una data di scadenza",
"max_tokens": "Token massimi",
"max_tokens_help": "Numero massimo di volte in cui è possibile accedere a questa condivisione. 0 significa nessun limite",
"view_manage": "Visualizza e gestisci condivisioni",
"no_share": "Nessuna condivisione",
"password_protected": "Protetto da password.",
"expiration_date": "Scadenza: {{- val, datetime}}. ",
"last_use": "Ultimo utilizzo: {{- val, datetime}}. ",
"usage": "Utilizzo: {{used}}/{{total}}. ",
"used_tokens": "Token usati: {{used}}. ",
"scope_invalid": "Ambito non valido",
"max_tokens_invalid": "Token massimi non validi",
"expiration_invalid": "Scadenza non valida",
"err_no_password": "Non sei autorizzato a condividere file/cartelle senza password",
"expiration_out_of_range": "Imposta una data di scadenza e assicurati che sia conforme alla politica aziendale, ad es. non è troppo lontan nel futuro",
"generic": "Errore imprevisto durante il salvataggio della condivisione",
"path_required": "È necessario almeno un percorso",
"path_write_scope": "L'ambito di scrittura richiede esattamente un percorso",
"nested_paths": "I percorsi non possono essere contenuti l'uno dentro l'altro",
"expiration_past": "La data di scadenza deve essere futura",
"usage_exceed": "Utilizzo massimo della condivisione superato",
"expired": "La condivisione è scaduta",
"browsable_multiple_paths": "Una condivisione con più percorsi non è navigabile",
"browsable_non_dir": "La condivisione non è una directory quindi non è navigabile"
},
"select2": {
"no_results": "Nessun risultato trovato",
"searching": "Ricerca...",
"removeall": "Rimuovi tutti gli elementi",
"remove": "Rimuovi elemento"
},
"change_pwd": {
"info": "Inserisci la password attuale, per ragioni di sicurezza, e poi la nuova password due volte, per verificare di averla scritta correttamente. Verrai disconnesso dopo aver modificato la tua password",
"current": "Password attuale",
"new": "Nuova password",
"confirm": "Conferma nuova password",
"save": "Modifica la mia password",
"required_fields": "Si prega di fornire la password attuale e quella nuova due volte",
"no_match": "I due campi della password non corrispondono",
"no_different": "La nuova password deve essere diversa da quella attuale",
"current_no_match": "La password attuale non corrisponde",
"generic": "Errore imprevisto durante la modifica della password",
"required": "È richiesta la modifica della password. Imposta una nuova password per continuare a utilizzare il tuo account"
},
"user": {
"username_reserved": "Il nome utente specificato è riservato",
"username_invalid": "Il nome utente specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~",
"home_required": "La directory principale è obbligatoria",
"home_invalid": "La directory principale deve essere un path assoluto",
"pub_key_invalid": "Chiave pubblica non valida",
"err_primary_group": "È consentito un solo gruppo primario",
"err_duplicate_group": "Rilevati gruppi duplicati",
"no_permissions": "I permessi per le directory sono obbligatori",
"no_root_permissions": "I permessi per la directory principale sono obbligatori",
"err_permissions_generic": "Permessi non validi: assicurati di utilizzare percorsi assoluti validi",
"2fa_invalid": "Configurazione non valida per l'autenticazione a due fattori",
"recovery_codes_invalid": "Codici di ripristino non validi",
"folde_path_required": "Il percorso di montaggio delle cartelle virtuali è obbligatorio",
"folder_duplicated": "Rilevate cartelle virtuali duplicate",
"folder_overlapped": "Rilevate cartelle virtuali sovrapposte",
"folder_quota_size_invalid": "La quota come dimensione delle cartelle virtuali deve essere maggiore o uguale a -1",
"folder_quota_file_invalid": "La quota come numero di file delle cartelle virtuali deve essere maggiore o uguale a -1",
"folder_quota_invalid": "Le quote come dimensione e numero di file delle cartelle virtuali devono essere entrambe -1 o maggiore o uguale di zero",
"ip_filters_invalid": "Filtri IP non validi, assicurati che rispettino la notazione CIDR, ad esempio 192.168.1.0/24",
"src_bw_limits_invalid": "Limiti di velocità della larghezza di banda per origine non validi",
"share_expiration_invalid": "La scadenza per le condivisioni deve essere maggiore o uguale al valore di default definito",
"file_pattern_path_invalid": "I percorsi dei filtri sui modelli di nome file devono essere assoluti",
"file_pattern_duplicated": "Rilevati filtri su modelli di nome file duplicati",
"file_pattern_invalid": "Filtri su modelli di nome file non validi",
"disable_active_2fa": "L'autenticazione a due fattori non può essere disabilitata per un utente con una configurazione attiva",
"pwd_change_conflict": "Non è possibile richiedere la modifica della password e allo stesso tempo impedire la modifica della password"
}
}

2297
static/vendor/i18next/i18next.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,420 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextBrowserLanguageDetector = factory());
})(this, (function () { 'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
var arr = [];
var each = arr.forEach;
var slice = arr.slice;
function defaults(obj) {
each.call(slice.call(arguments, 1), function (source) {
if (source) {
for (var prop in source) {
if (obj[prop] === undefined) obj[prop] = source[prop];
}
}
});
return obj;
}
// eslint-disable-next-line no-control-regex
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
var serializeCookie = function serializeCookie(name, val, options) {
var opt = options || {};
opt.path = opt.path || '/';
var value = encodeURIComponent(val);
var str = "".concat(name, "=").concat(value);
if (opt.maxAge > 0) {
var maxAge = opt.maxAge - 0;
if (Number.isNaN(maxAge)) throw new Error('maxAge should be a Number');
str += "; Max-Age=".concat(Math.floor(maxAge));
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError('option domain is invalid');
}
str += "; Domain=".concat(opt.domain);
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError('option path is invalid');
}
str += "; Path=".concat(opt.path);
}
if (opt.expires) {
if (typeof opt.expires.toUTCString !== 'function') {
throw new TypeError('option expires is invalid');
}
str += "; Expires=".concat(opt.expires.toUTCString());
}
if (opt.httpOnly) str += '; HttpOnly';
if (opt.secure) str += '; Secure';
if (opt.sameSite) {
var sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite;
switch (sameSite) {
case true:
str += '; SameSite=Strict';
break;
case 'lax':
str += '; SameSite=Lax';
break;
case 'strict':
str += '; SameSite=Strict';
break;
case 'none':
str += '; SameSite=None';
break;
default:
throw new TypeError('option sameSite is invalid');
}
}
return str;
};
var cookie = {
create: function create(name, value, minutes, domain) {
var cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {
path: '/',
sameSite: 'strict'
};
if (minutes) {
cookieOptions.expires = new Date();
cookieOptions.expires.setTime(cookieOptions.expires.getTime() + minutes * 60 * 1000);
}
if (domain) cookieOptions.domain = domain;
document.cookie = serializeCookie(name, encodeURIComponent(value), cookieOptions);
},
read: function read(name) {
var nameEQ = "".concat(name, "=");
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
},
remove: function remove(name) {
this.create(name, '', -1);
}
};
var cookie$1 = {
name: 'cookie',
lookup: function lookup(options) {
var found;
if (options.lookupCookie && typeof document !== 'undefined') {
var c = cookie.read(options.lookupCookie);
if (c) found = c;
}
return found;
},
cacheUserLanguage: function cacheUserLanguage(lng, options) {
if (options.lookupCookie && typeof document !== 'undefined') {
cookie.create(options.lookupCookie, lng, options.cookieMinutes, options.cookieDomain, options.cookieOptions);
}
}
};
var querystring = {
name: 'querystring',
lookup: function lookup(options) {
var found;
if (typeof window !== 'undefined') {
var search = window.location.search;
if (!window.location.search && window.location.hash && window.location.hash.indexOf('?') > -1) {
search = window.location.hash.substring(window.location.hash.indexOf('?'));
}
var query = search.substring(1);
var params = query.split('&');
for (var i = 0; i < params.length; i++) {
var pos = params[i].indexOf('=');
if (pos > 0) {
var key = params[i].substring(0, pos);
if (key === options.lookupQuerystring) {
found = params[i].substring(pos + 1);
}
}
}
}
return found;
}
};
var hasLocalStorageSupport = null;
var localStorageAvailable = function localStorageAvailable() {
if (hasLocalStorageSupport !== null) return hasLocalStorageSupport;
try {
hasLocalStorageSupport = window !== 'undefined' && window.localStorage !== null;
var testKey = 'i18next.translate.boo';
window.localStorage.setItem(testKey, 'foo');
window.localStorage.removeItem(testKey);
} catch (e) {
hasLocalStorageSupport = false;
}
return hasLocalStorageSupport;
};
var localStorage = {
name: 'localStorage',
lookup: function lookup(options) {
var found;
if (options.lookupLocalStorage && localStorageAvailable()) {
var lng = window.localStorage.getItem(options.lookupLocalStorage);
if (lng) found = lng;
}
return found;
},
cacheUserLanguage: function cacheUserLanguage(lng, options) {
if (options.lookupLocalStorage && localStorageAvailable()) {
window.localStorage.setItem(options.lookupLocalStorage, lng);
}
}
};
var hasSessionStorageSupport = null;
var sessionStorageAvailable = function sessionStorageAvailable() {
if (hasSessionStorageSupport !== null) return hasSessionStorageSupport;
try {
hasSessionStorageSupport = window !== 'undefined' && window.sessionStorage !== null;
var testKey = 'i18next.translate.boo';
window.sessionStorage.setItem(testKey, 'foo');
window.sessionStorage.removeItem(testKey);
} catch (e) {
hasSessionStorageSupport = false;
}
return hasSessionStorageSupport;
};
var sessionStorage = {
name: 'sessionStorage',
lookup: function lookup(options) {
var found;
if (options.lookupSessionStorage && sessionStorageAvailable()) {
var lng = window.sessionStorage.getItem(options.lookupSessionStorage);
if (lng) found = lng;
}
return found;
},
cacheUserLanguage: function cacheUserLanguage(lng, options) {
if (options.lookupSessionStorage && sessionStorageAvailable()) {
window.sessionStorage.setItem(options.lookupSessionStorage, lng);
}
}
};
var navigator$1 = {
name: 'navigator',
lookup: function lookup(options) {
var found = [];
if (typeof navigator !== 'undefined') {
if (navigator.languages) {
// chrome only; not an array, so can't use .push.apply instead of iterating
for (var i = 0; i < navigator.languages.length; i++) {
found.push(navigator.languages[i]);
}
}
if (navigator.userLanguage) {
found.push(navigator.userLanguage);
}
if (navigator.language) {
found.push(navigator.language);
}
}
return found.length > 0 ? found : undefined;
}
};
var htmlTag = {
name: 'htmlTag',
lookup: function lookup(options) {
var found;
var htmlTag = options.htmlTag || (typeof document !== 'undefined' ? document.documentElement : null);
if (htmlTag && typeof htmlTag.getAttribute === 'function') {
found = htmlTag.getAttribute('lang');
}
return found;
}
};
var path = {
name: 'path',
lookup: function lookup(options) {
var found;
if (typeof window !== 'undefined') {
var language = window.location.pathname.match(/\/([a-zA-Z-]*)/g);
if (language instanceof Array) {
if (typeof options.lookupFromPathIndex === 'number') {
if (typeof language[options.lookupFromPathIndex] !== 'string') {
return undefined;
}
found = language[options.lookupFromPathIndex].replace('/', '');
} else {
found = language[0].replace('/', '');
}
}
}
return found;
}
};
var subdomain = {
name: 'subdomain',
lookup: function lookup(options) {
// If given get the subdomain index else 1
var lookupFromSubdomainIndex = typeof options.lookupFromSubdomainIndex === 'number' ? options.lookupFromSubdomainIndex + 1 : 1;
// get all matches if window.location. is existing
// first item of match is the match itself and the second is the first group macht which sould be the first subdomain match
// is the hostname no public domain get the or option of localhost
var language = typeof window !== 'undefined' && window.location && window.location.hostname && window.location.hostname.match(/^(\w{2,5})\.(([a-z0-9-]{1,63}\.[a-z]{2,6})|localhost)/i);
// if there is no match (null) return undefined
if (!language) return undefined;
// return the given group match
return language[lookupFromSubdomainIndex];
}
};
function getDefaults() {
return {
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
lookupSessionStorage: 'i18nextLng',
// cache user language
caches: ['localStorage'],
excludeCacheFor: ['cimode'],
// cookieMinutes: 10,
// cookieDomain: 'myDomain'
convertDetectedLanguage: function convertDetectedLanguage(l) {
return l;
}
};
}
var Browser = /*#__PURE__*/function () {
function Browser(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Browser);
this.type = 'languageDetector';
this.detectors = {};
this.init(services, options);
}
_createClass(Browser, [{
key: "init",
value: function init(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var i18nOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
this.services = services || {
languageUtils: {}
}; // this way the language detector can be used without i18next
this.options = defaults(options, this.options || {}, getDefaults());
if (typeof this.options.convertDetectedLanguage === 'string' && this.options.convertDetectedLanguage.indexOf('15897') > -1) {
this.options.convertDetectedLanguage = function (l) {
return l.replace('-', '_');
};
}
// backwards compatibility
if (this.options.lookupFromUrlIndex) this.options.lookupFromPathIndex = this.options.lookupFromUrlIndex;
this.i18nOptions = i18nOptions;
this.addDetector(cookie$1);
this.addDetector(querystring);
this.addDetector(localStorage);
this.addDetector(sessionStorage);
this.addDetector(navigator$1);
this.addDetector(htmlTag);
this.addDetector(path);
this.addDetector(subdomain);
}
}, {
key: "addDetector",
value: function addDetector(detector) {
this.detectors[detector.name] = detector;
}
}, {
key: "detect",
value: function detect(detectionOrder) {
var _this = this;
if (!detectionOrder) detectionOrder = this.options.order;
var detected = [];
detectionOrder.forEach(function (detectorName) {
if (_this.detectors[detectorName]) {
var lookup = _this.detectors[detectorName].lookup(_this.options);
if (lookup && typeof lookup === 'string') lookup = [lookup];
if (lookup) detected = detected.concat(lookup);
}
});
detected = detected.map(function (d) {
return _this.options.convertDetectedLanguage(d);
});
if (this.services.languageUtils.getBestMatchFromCodes) return detected; // new i18next v19.5.0
return detected.length > 0 ? detected[0] : null; // a little backward compatibility
}
}, {
key: "cacheUserLanguage",
value: function cacheUserLanguage(lng, caches) {
var _this2 = this;
if (!caches) caches = this.options.caches;
if (!caches) return;
if (this.options.excludeCacheFor && this.options.excludeCacheFor.indexOf(lng) > -1) return;
caches.forEach(function (cacheName) {
if (_this2.detectors[cacheName]) _this2.detectors[cacheName].cacheUserLanguage(lng, _this2.options);
});
}
}]);
return Browser;
}();
Browser.type = 'languageDetector';
return Browser;
}));

View file

@ -0,0 +1,266 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextChainedBackend = factory());
})(this, (function () { 'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
var arr = [];
var each = arr.forEach;
var slice = arr.slice;
function defaults(obj) {
each.call(slice.call(arguments, 1), function (source) {
if (source) {
for (var prop in source) {
if (obj[prop] === undefined) obj[prop] = source[prop];
}
}
});
return obj;
}
function createClassOnDemand(ClassOrObject) {
if (!ClassOrObject) return null;
if (typeof ClassOrObject === 'function') return new ClassOrObject();
return ClassOrObject;
}
function getDefaults() {
return {
handleEmptyResourcesAsFailed: true,
cacheHitMode: 'none'
// reloadInterval: typeof window !== 'undefined' ? false : 60 * 60 * 1000
// refreshExpirationTime: 60 * 60 * 1000
};
}
function handleCorrectReadFunction(backend, language, namespace, resolver) {
var fc = backend.read.bind(backend);
if (fc.length === 2) {
// no callback
try {
var r = fc(language, namespace);
if (r && typeof r.then === 'function') {
// promise
r.then(function (data) {
return resolver(null, data);
})["catch"](resolver);
} else {
// sync
resolver(null, r);
}
} catch (err) {
resolver(err);
}
return;
}
// normal with callback
fc(language, namespace, resolver);
}
var Backend = /*#__PURE__*/function () {
function Backend(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var i18nextOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
_classCallCheck(this, Backend);
this.backends = [];
this.type = 'backend';
this.allOptions = i18nextOptions;
this.init(services, options);
}
_createClass(Backend, [{
key: "init",
value: function init(services) {
var _this = this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var i18nextOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
this.services = services;
this.options = defaults(options, this.options || {}, getDefaults());
this.allOptions = i18nextOptions;
this.options.backends && this.options.backends.forEach(function (b, i) {
_this.backends[i] = _this.backends[i] || createClassOnDemand(b);
_this.backends[i].init(services, _this.options.backendOptions && _this.options.backendOptions[i] || {}, i18nextOptions);
});
if (this.services && this.options.reloadInterval) {
setInterval(function () {
return _this.reload();
}, this.options.reloadInterval);
}
}
}, {
key: "read",
value: function read(language, namespace, callback) {
var _this2 = this;
var bLen = this.backends.length;
var loadPosition = function loadPosition(pos) {
if (pos >= bLen) return callback(new Error('non of the backend loaded data', true)); // failed pass retry flag
var isLastBackend = pos === bLen - 1;
var lengthCheckAmount = _this2.options.handleEmptyResourcesAsFailed && !isLastBackend ? 0 : -1;
var backend = _this2.backends[pos];
if (backend.read) {
handleCorrectReadFunction(backend, language, namespace, function (err, data, savedAt) {
if (!err && data && Object.keys(data).length > lengthCheckAmount) {
callback(null, data, pos);
savePosition(pos - 1, data); // save one in front
if (backend.save && _this2.options.cacheHitMode && ['refresh', 'refreshAndUpdateStore'].indexOf(_this2.options.cacheHitMode) > -1) {
if (savedAt && _this2.options.refreshExpirationTime && savedAt + _this2.options.refreshExpirationTime > Date.now()) return;
var nextBackend = _this2.backends[pos + 1];
if (nextBackend && nextBackend.read) {
handleCorrectReadFunction(nextBackend, language, namespace, function (err, data) {
if (err) return;
if (!data) return;
if (Object.keys(data).length <= lengthCheckAmount) return;
savePosition(pos, data);
if (_this2.options.cacheHitMode !== 'refreshAndUpdateStore') return;
if (_this2.services && _this2.services.resourceStore) {
_this2.services.resourceStore.addResourceBundle(language, namespace, data);
}
});
}
}
} else {
loadPosition(pos + 1); // try load from next
}
});
} else {
loadPosition(pos + 1); // try load from next
}
};
var savePosition = function savePosition(pos, data) {
if (pos < 0) return;
var backend = _this2.backends[pos];
if (backend.save) {
backend.save(language, namespace, data);
savePosition(pos - 1, data);
} else {
savePosition(pos - 1, data);
}
};
loadPosition(0);
}
}, {
key: "create",
value: function create(languages, namespace, key, fallbackValue) {
var clb = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : function () {};
var opts = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};
this.backends.forEach(function (b) {
if (!b.create) return;
var fc = b.create.bind(b);
if (fc.length < 6) {
// no callback
try {
var r;
if (fc.length === 5) {
// future callback-less api for i18next-locize-backend
r = fc(languages, namespace, key, fallbackValue, opts);
} else {
r = fc(languages, namespace, key, fallbackValue);
}
if (r && typeof r.then === 'function') {
// promise
r.then(function (data) {
return clb(null, data);
})["catch"](clb);
} else {
// sync
clb(null, r);
}
} catch (err) {
clb(err);
}
return;
}
// normal with callback
fc(languages, namespace, key, fallbackValue, clb /* unused callback */, opts);
});
}
}, {
key: "reload",
value: function reload() {
var _this3 = this;
var _this$services = this.services,
backendConnector = _this$services.backendConnector,
languageUtils = _this$services.languageUtils,
logger = _this$services.logger;
var currentLanguage = backendConnector.language;
if (currentLanguage && currentLanguage.toLowerCase() === 'cimode') return; // avoid loading resources for cimode
var toLoad = [];
var append = function append(lng) {
var lngs = languageUtils.toResolveHierarchy(lng);
lngs.forEach(function (l) {
if (toLoad.indexOf(l) < 0) toLoad.push(l);
});
};
append(currentLanguage);
if (this.allOptions.preload) this.allOptions.preload.forEach(function (l) {
return append(l);
});
toLoad.forEach(function (lng) {
_this3.allOptions.ns.forEach(function (ns) {
backendConnector.read(lng, ns, 'read', null, null, function (err, data) {
if (err) logger.warn("loading namespace ".concat(ns, " for language ").concat(lng, " failed"), err);
if (!err && data) logger.log("loaded namespace ".concat(ns, " for language ").concat(lng), data);
backendConnector.loaded("".concat(lng, "|").concat(ns), err, data);
});
});
});
}
}]);
return Backend;
}();
Backend.type = 'backend';
return Backend;
}));

View file

@ -0,0 +1,414 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.i18nextHttpBackend = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
(function (global){(function (){
var fetchApi
if (typeof fetch === 'function') {
if (typeof global !== 'undefined' && global.fetch) {
fetchApi = global.fetch
} else if (typeof window !== 'undefined' && window.fetch) {
fetchApi = window.fetch
} else {
fetchApi = fetch
}
}
if (typeof require !== 'undefined' && (typeof window === 'undefined' || typeof window.document === 'undefined')) {
var f = fetchApi || require('cross-fetch')
if (f.default) f = f.default
exports.default = f
module.exports = exports.default
}
}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"cross-fetch":5}],2:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils.js");
var _request = _interopRequireDefault(require("./request.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
var getDefaults = function getDefaults() {
return {
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/add/{{lng}}/{{ns}}',
parse: function parse(data) {
return JSON.parse(data);
},
stringify: JSON.stringify,
parsePayload: function parsePayload(namespace, key, fallbackValue) {
return _defineProperty({}, key, fallbackValue || '');
},
parseLoadPayload: function parseLoadPayload(languages, namespaces) {
return undefined;
},
request: _request.default,
reloadInterval: typeof window !== 'undefined' ? false : 60 * 60 * 1000,
customHeaders: {},
queryStringParams: {},
crossDomain: false,
withCredentials: false,
overrideMimeType: false,
requestOptions: {
mode: 'cors',
credentials: 'same-origin',
cache: 'default'
}
};
};
var Backend = function () {
function Backend(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var allOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
_classCallCheck(this, Backend);
this.services = services;
this.options = options;
this.allOptions = allOptions;
this.type = 'backend';
this.init(services, options, allOptions);
}
_createClass(Backend, [{
key: "init",
value: function init(services) {
var _this = this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var allOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
this.services = services;
this.options = (0, _utils.defaults)(options, this.options || {}, getDefaults());
this.allOptions = allOptions;
if (this.services && this.options.reloadInterval) {
setInterval(function () {
return _this.reload();
}, this.options.reloadInterval);
}
}
}, {
key: "readMulti",
value: function readMulti(languages, namespaces, callback) {
this._readAny(languages, languages, namespaces, namespaces, callback);
}
}, {
key: "read",
value: function read(language, namespace, callback) {
this._readAny([language], language, [namespace], namespace, callback);
}
}, {
key: "_readAny",
value: function _readAny(languages, loadUrlLanguages, namespaces, loadUrlNamespaces, callback) {
var _this2 = this;
var loadPath = this.options.loadPath;
if (typeof this.options.loadPath === 'function') {
loadPath = this.options.loadPath(languages, namespaces);
}
loadPath = (0, _utils.makePromise)(loadPath);
loadPath.then(function (resolvedLoadPath) {
if (!resolvedLoadPath) return callback(null, {});
var url = _this2.services.interpolator.interpolate(resolvedLoadPath, {
lng: languages.join('+'),
ns: namespaces.join('+')
});
_this2.loadUrl(url, callback, loadUrlLanguages, loadUrlNamespaces);
});
}
}, {
key: "loadUrl",
value: function loadUrl(url, callback, languages, namespaces) {
var _this3 = this;
var lng = typeof languages === 'string' ? [languages] : languages;
var ns = typeof namespaces === 'string' ? [namespaces] : namespaces;
var payload = this.options.parseLoadPayload(lng, ns);
this.options.request(this.options, url, payload, function (err, res) {
if (res && (res.status >= 500 && res.status < 600 || !res.status)) return callback('failed loading ' + url + '; status code: ' + res.status, true);
if (res && res.status >= 400 && res.status < 500) return callback('failed loading ' + url + '; status code: ' + res.status, false);
if (!res && err && err.message && err.message.indexOf('Failed to fetch') > -1) return callback('failed loading ' + url + ': ' + err.message, true);
if (err) return callback(err, false);
var ret, parseErr;
try {
if (typeof res.data === 'string') {
ret = _this3.options.parse(res.data, languages, namespaces);
} else {
ret = res.data;
}
} catch (e) {
parseErr = 'failed parsing ' + url + ' to json';
}
if (parseErr) return callback(parseErr, false);
callback(null, ret);
});
}
}, {
key: "create",
value: function create(languages, namespace, key, fallbackValue, callback) {
var _this4 = this;
if (!this.options.addPath) return;
if (typeof languages === 'string') languages = [languages];
var payload = this.options.parsePayload(namespace, key, fallbackValue);
var finished = 0;
var dataArray = [];
var resArray = [];
languages.forEach(function (lng) {
var addPath = _this4.options.addPath;
if (typeof _this4.options.addPath === 'function') {
addPath = _this4.options.addPath(lng, namespace);
}
var url = _this4.services.interpolator.interpolate(addPath, {
lng: lng,
ns: namespace
});
_this4.options.request(_this4.options, url, payload, function (data, res) {
finished += 1;
dataArray.push(data);
resArray.push(res);
if (finished === languages.length) {
if (typeof callback === 'function') callback(dataArray, resArray);
}
});
});
}
}, {
key: "reload",
value: function reload() {
var _this5 = this;
var _this$services = this.services,
backendConnector = _this$services.backendConnector,
languageUtils = _this$services.languageUtils,
logger = _this$services.logger;
var currentLanguage = backendConnector.language;
if (currentLanguage && currentLanguage.toLowerCase() === 'cimode') return;
var toLoad = [];
var append = function append(lng) {
var lngs = languageUtils.toResolveHierarchy(lng);
lngs.forEach(function (l) {
if (toLoad.indexOf(l) < 0) toLoad.push(l);
});
};
append(currentLanguage);
if (this.allOptions.preload) this.allOptions.preload.forEach(function (l) {
return append(l);
});
toLoad.forEach(function (lng) {
_this5.allOptions.ns.forEach(function (ns) {
backendConnector.read(lng, ns, 'read', null, null, function (err, data) {
if (err) logger.warn("loading namespace ".concat(ns, " for language ").concat(lng, " failed"), err);
if (!err && data) logger.log("loaded namespace ".concat(ns, " for language ").concat(lng), data);
backendConnector.loaded("".concat(lng, "|").concat(ns), err, data);
});
});
});
}
}]);
return Backend;
}();
Backend.type = 'backend';
var _default = exports.default = Backend;
module.exports = exports.default;
},{"./request.js":3,"./utils.js":4}],3:[function(require,module,exports){
(function (global){(function (){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _utils = require("./utils.js");
var fetchNode = _interopRequireWildcard(require("./getFetch.js"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
var fetchApi;
if (typeof fetch === 'function') {
if (typeof global !== 'undefined' && global.fetch) {
fetchApi = global.fetch;
} else if (typeof window !== 'undefined' && window.fetch) {
fetchApi = window.fetch;
} else {
fetchApi = fetch;
}
}
var XmlHttpRequestApi;
if ((0, _utils.hasXMLHttpRequest)()) {
if (typeof global !== 'undefined' && global.XMLHttpRequest) {
XmlHttpRequestApi = global.XMLHttpRequest;
} else if (typeof window !== 'undefined' && window.XMLHttpRequest) {
XmlHttpRequestApi = window.XMLHttpRequest;
}
}
var ActiveXObjectApi;
if (typeof ActiveXObject === 'function') {
if (typeof global !== 'undefined' && global.ActiveXObject) {
ActiveXObjectApi = global.ActiveXObject;
} else if (typeof window !== 'undefined' && window.ActiveXObject) {
ActiveXObjectApi = window.ActiveXObject;
}
}
if (!fetchApi && fetchNode && !XmlHttpRequestApi && !ActiveXObjectApi) fetchApi = fetchNode.default || fetchNode;
if (typeof fetchApi !== 'function') fetchApi = undefined;
var addQueryString = function addQueryString(url, params) {
if (params && _typeof(params) === 'object') {
var queryString = '';
for (var paramName in params) {
queryString += '&' + encodeURIComponent(paramName) + '=' + encodeURIComponent(params[paramName]);
}
if (!queryString) return url;
url = url + (url.indexOf('?') !== -1 ? '&' : '?') + queryString.slice(1);
}
return url;
};
var fetchIt = function fetchIt(url, fetchOptions, callback) {
var resolver = function resolver(response) {
if (!response.ok) return callback(response.statusText || 'Error', {
status: response.status
});
response.text().then(function (data) {
callback(null, {
status: response.status,
data: data
});
}).catch(callback);
};
if (typeof fetch === 'function') {
fetch(url, fetchOptions).then(resolver).catch(callback);
} else {
fetchApi(url, fetchOptions).then(resolver).catch(callback);
}
};
var omitFetchOptions = false;
var requestWithFetch = function requestWithFetch(options, url, payload, callback) {
if (options.queryStringParams) {
url = addQueryString(url, options.queryStringParams);
}
var headers = (0, _utils.defaults)({}, typeof options.customHeaders === 'function' ? options.customHeaders() : options.customHeaders);
if (typeof window === 'undefined' && typeof global !== 'undefined' && typeof global.process !== 'undefined' && global.process.versions && global.process.versions.node) {
headers['User-Agent'] = "i18next-http-backend (node/".concat(global.process.version, "; ").concat(global.process.platform, " ").concat(global.process.arch, ")");
}
if (payload) headers['Content-Type'] = 'application/json';
var reqOptions = typeof options.requestOptions === 'function' ? options.requestOptions(payload) : options.requestOptions;
var fetchOptions = (0, _utils.defaults)({
method: payload ? 'POST' : 'GET',
body: payload ? options.stringify(payload) : undefined,
headers: headers
}, omitFetchOptions ? {} : reqOptions);
try {
fetchIt(url, fetchOptions, callback);
} catch (e) {
if (!reqOptions || Object.keys(reqOptions).length === 0 || !e.message || e.message.indexOf('not implemented') < 0) {
return callback(e);
}
try {
Object.keys(reqOptions).forEach(function (opt) {
delete fetchOptions[opt];
});
fetchIt(url, fetchOptions, callback);
omitFetchOptions = true;
} catch (err) {
callback(err);
}
}
};
var requestWithXmlHttpRequest = function requestWithXmlHttpRequest(options, url, payload, callback) {
if (payload && _typeof(payload) === 'object') {
payload = addQueryString('', payload).slice(1);
}
if (options.queryStringParams) {
url = addQueryString(url, options.queryStringParams);
}
try {
var x;
if (XmlHttpRequestApi) {
x = new XmlHttpRequestApi();
} else {
x = new ActiveXObjectApi('MSXML2.XMLHTTP.3.0');
}
x.open(payload ? 'POST' : 'GET', url, 1);
if (!options.crossDomain) {
x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
x.withCredentials = !!options.withCredentials;
if (payload) {
x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
if (x.overrideMimeType) {
x.overrideMimeType('application/json');
}
var h = options.customHeaders;
h = typeof h === 'function' ? h() : h;
if (h) {
for (var i in h) {
x.setRequestHeader(i, h[i]);
}
}
x.onreadystatechange = function () {
x.readyState > 3 && callback(x.status >= 400 ? x.statusText : null, {
status: x.status,
data: x.responseText
});
};
x.send(payload);
} catch (e) {
console && console.log(e);
}
};
var request = function request(options, url, payload, callback) {
if (typeof payload === 'function') {
callback = payload;
payload = undefined;
}
callback = callback || function () {};
if (fetchApi && url.indexOf('file:') !== 0) {
return requestWithFetch(options, url, payload, callback);
}
if ((0, _utils.hasXMLHttpRequest)() || typeof ActiveXObject === 'function') {
return requestWithXmlHttpRequest(options, url, payload, callback);
}
callback(new Error('No fetch and no xhr implementation found!'));
};
var _default = exports.default = request;
module.exports = exports.default;
}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./getFetch.js":1,"./utils.js":4}],4:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.defaults = defaults;
exports.hasXMLHttpRequest = hasXMLHttpRequest;
exports.makePromise = makePromise;
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
var arr = [];
var each = arr.forEach;
var slice = arr.slice;
function defaults(obj) {
each.call(slice.call(arguments, 1), function (source) {
if (source) {
for (var prop in source) {
if (obj[prop] === undefined) obj[prop] = source[prop];
}
}
});
return obj;
}
function hasXMLHttpRequest() {
return typeof XMLHttpRequest === 'function' || (typeof XMLHttpRequest === "undefined" ? "undefined" : _typeof(XMLHttpRequest)) === 'object';
}
function isPromise(maybePromise) {
return !!maybePromise && typeof maybePromise.then === 'function';
}
function makePromise(maybePromise) {
if (isPromise(maybePromise)) {
return maybePromise;
}
return Promise.resolve(maybePromise);
}
},{}],5:[function(require,module,exports){
},{}]},{},[2])(2)
});

View file

@ -0,0 +1,190 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextLocalStorageBackend = factory());
})(this, (function () { 'use strict';
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _defineProperty(obj, key, value) {
key = _toPropertyKey(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/* eslint-disable max-classes-per-file */
var Storage = /*#__PURE__*/function () {
function Storage(options) {
_classCallCheck(this, Storage);
this.store = options.store;
}
_createClass(Storage, [{
key: "setItem",
value: function setItem(key, value) {
if (this.store) {
try {
this.store.setItem(key, value);
} catch (e) {
// f.log('failed to set value for key "' + key + '" to localStorage.');
}
}
}
}, {
key: "getItem",
value: function getItem(key, value) {
if (this.store) {
try {
return this.store.getItem(key, value);
} catch (e) {
// f.log('failed to get value for key "' + key + '" from localStorage.');
}
}
return undefined;
}
}]);
return Storage;
}();
function getDefaults() {
var store = null;
try {
store = window.localStorage;
} catch (e) {
if (typeof window !== 'undefined') {
console.log('Failed to load local storage.', e);
}
}
return {
prefix: 'i18next_res_',
expirationTime: 7 * 24 * 60 * 60 * 1000,
defaultVersion: undefined,
versions: {},
store: store
};
}
var Cache = /*#__PURE__*/function () {
function Cache(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Cache);
this.init(services, options);
this.type = 'backend';
}
_createClass(Cache, [{
key: "init",
value: function init(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.services = services;
this.options = _objectSpread(_objectSpread(_objectSpread({}, getDefaults()), this.options), options);
this.storage = new Storage(this.options);
}
}, {
key: "read",
value: function read(language, namespace, callback) {
var nowMS = Date.now();
if (!this.storage.store) {
return callback(null, null);
}
var local = this.storage.getItem("".concat(this.options.prefix).concat(language, "-").concat(namespace));
if (local) {
local = JSON.parse(local);
var version = this.getVersion(language);
if (
// expiration field is mandatory, and should not be expired
local.i18nStamp && local.i18nStamp + this.options.expirationTime > nowMS &&
// there should be no language version set, or if it is, it should match the one in translation
version === local.i18nVersion) {
var i18nStamp = local.i18nStamp;
delete local.i18nVersion;
delete local.i18nStamp;
return callback(null, local, i18nStamp);
}
}
return callback(null, null);
}
}, {
key: "save",
value: function save(language, namespace, data) {
if (this.storage.store) {
data.i18nStamp = Date.now();
// language version (if set)
var version = this.getVersion(language);
if (version) {
data.i18nVersion = version;
}
// save
this.storage.setItem("".concat(this.options.prefix).concat(language, "-").concat(namespace), JSON.stringify(data));
}
}
}, {
key: "getVersion",
value: function getVersion(language) {
return this.options.versions[language] || this.options.defaultVersion;
}
}]);
return Cache;
}();
Cache.type = 'backend';
return Cache;
}));

128
static/vendor/i18next/jquery-i18next.js vendored Normal file
View file

@ -0,0 +1,128 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.jqueryI18next = factory());
}(this, (function () { 'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var defaults = {
tName: 't',
i18nName: 'i18n',
handleName: 'localize',
selectorAttr: 'data-i18n',
targetAttr: 'i18n-target',
optionsAttr: 'i18n-options',
useOptionsAttr: false,
parseDefaultValueFromContent: true
};
function init(i18next, $) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
options = _extends({}, defaults, options);
function parse(ele, key, opts) {
if (key.length === 0) return;
var attr = 'text';
if (key.indexOf('[') === 0) {
var parts = key.split(']');
key = parts[1];
attr = parts[0].substr(1, parts[0].length - 1);
}
if (key.indexOf(';') === key.length - 1) {
key = key.substr(0, key.length - 2);
}
function extendDefault(o, val) {
if (!options.parseDefaultValueFromContent) return o;
return _extends({}, o, { defaultValue: val });
}
if (attr === 'html') {
ele.html(i18next.t(key, extendDefault(opts, ele.html())));
} else if (attr === 'text') {
ele.text(i18next.t(key, extendDefault(opts, ele.text())));
} else if (attr === 'prepend') {
ele.prepend(i18next.t(key, extendDefault(opts, ele.html())));
} else if (attr === 'append') {
ele.append(i18next.t(key, extendDefault(opts, ele.html())));
} else if (attr.indexOf('data-') === 0) {
var dataAttr = attr.substr('data-'.length);
var translated = i18next.t(key, extendDefault(opts, ele.data(dataAttr)));
// we change into the data cache
ele.data(dataAttr, translated);
// we change into the dom
ele.attr(attr, translated);
} else {
ele.attr(attr, i18next.t(key, extendDefault(opts, ele.attr(attr))));
}
}
function localize(ele, opts) {
var key = ele.attr(options.selectorAttr);
if (!key && typeof key !== 'undefined' && key !== false) key = ele.text() || ele.val();
if (!key) return;
var target = ele,
targetSelector = ele.data(options.targetAttr);
if (targetSelector) target = ele.find(targetSelector) || ele;
if (!opts && options.useOptionsAttr === true) opts = ele.data(options.optionsAttr);
opts = opts || {};
if (key.indexOf(';') >= 0) {
var keys = key.split(';');
$.each(keys, function (m, k) {
// .trim(): Trim the comma-separated parameters on the data-i18n attribute.
if (k !== '') parse(target, k.trim(), opts);
});
} else {
parse(target, key, opts);
}
if (options.useOptionsAttr === true) {
var clone = {};
clone = _extends({ clone: clone }, opts);
delete clone.lng;
ele.data(options.optionsAttr, clone);
}
}
function handle(opts) {
return this.each(function () {
// localize element itself
localize($(this), opts);
// localize children
var elements = $(this).find('[' + options.selectorAttr + ']');
elements.each(function () {
localize($(this), opts);
});
});
};
// $.t $.i18n shortcut
$[options.tName] = i18next.t.bind(i18next);
$[options.i18nName] = i18next;
// selector function $(mySelector).localize(opts);
$.fn[options.handleName] = handle;
}
var index = {
init: init
};
return index;
})));

View file

@ -14,14 +14,14 @@ therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com). explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- define "errmsg"}} {{- define "errmsg"}}
<div id="errorMsg" class="{{if not . }}d-none {{end}}alert alert-dismissible bg-light-warning d-flex align-items-center p-5 mb-10"> <div id="errorMsg" class="{{if not . }}d-none {{end}}rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10">
<i class="ki-duotone ki-information-5 fs-3x text-warning me-5"> <i class="ki-duotone ki-information fs-3x text-warning me-5">
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
<span class="path3"></span> <span class="path3"></span>
</i> </i>
<div class="text-gray-800 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10"> <div class="text-gray-800 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10">
<span id="errorTxt">{{.}}</span> <span data-i18n="{{.}}" id="errorTxt"></span>
</div> </div>
<button id="id_dismiss_error_msg" type="button" class="position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto"> <button id="id_dismiss_error_msg" type="button" class="position-absolute position-sm-relative m-2 m-sm-0 top-0 end-0 btn btn-icon btn-sm btn-active-light-primary ms-sm-auto">
<i class="ki-duotone ki-cross fs-2x text-primary"> <i class="ki-duotone ki-cross fs-2x text-primary">
@ -71,7 +71,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- end}} {{- end}}
{{- define "basejs"}} {{- define "basejs"}}
<script type="text/javascript" {{- if .}} nonce="{{.}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode // https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode
function escapeHTML(str) { function escapeHTML(str) {
var div = document.createElement('div'); var div = document.createElement('div');
@ -106,6 +106,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
initEmpty: false, initEmpty: false,
show: function () { show: function () {
$(this).localize();
$(this).slideDown(); $(this).slideDown();
$(this).find('[data-repeater-delete]').on("click", function(e){ $(this).find('[data-repeater-delete]').on("click", function(e){
e.preventDefault(); e.preventDefault();
@ -124,6 +125,88 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
} }
const renderI18n = () => {
$('body').localize();
let select2elements = [].slice.call(document.querySelectorAll('[data-control="i18n-select2"]'));
select2elements.map(function (element){
if (element.getAttribute("data-kt-initialized") === "1") {
return;
}
let options = {
dir: document.body.getAttribute('direction'),
language: {
noResults: function () {
return $.t('select2.no_results');
},
searching: function () {
return $.t('select2.searching');
},
removeAllItems: function () {
return $.t('select2.removeall');
},
removeItem: function () {
return $.t('select2.remove');
},
search: function() {
return $.t('general.search');
}
}
};
if (element.getAttribute('data-hide-search') == 'true') {
options.minimumResultsForSearch = Infinity;
}
$(element).select2(options);
element.setAttribute("data-kt-initialized", "1");
});
}
function initLocalizer() {
i18next
.use(i18nextChainedBackend)
.use(i18nextBrowserLanguageDetector)
.init({
debug: false,
fallbackLng: 'en',
load: 'languageOnly',
backend: {
backends: [
i18nextLocalStorageBackend,
i18nextHttpBackend
],
backendOptions: [{
expirationTime: 7 * 24 * 60 * 60 * 1000, // 7 days
defaultVersion: '{{.Version}}'
}, {
loadPath: '{{.StaticURL}}/locales/{{"{{lng}}"}}/{{"{{ns}}"}}.json'
}
]
}
}, (err, t) => {
if (err) {
console.error(err);
} else {
jqueryI18next.init(i18next, $, { useOptionsAttr: true });
renderI18n();
$('title').text('{{.Branding.Name}} - '+$.t('{{.Title}}'));
$.event.trigger({
type: "i18nload"
});
}
$('#app_loader').addClass("d-none");
$('#app_root').removeClass("d-none");
$.event.trigger({
type: "i18nshow"
});
});
}
function setI18NData(el, value) {
el.attr("data-i18n", value);
el.localize();
}
KTUtil.onDOMContentLoaded(function () { KTUtil.onDOMContentLoaded(function () {
var dismissErrorBtn = $('#id_dismiss_error_msg'); var dismissErrorBtn = $('#id_dismiss_error_msg');
if (dismissErrorBtn){ if (dismissErrorBtn){
@ -139,6 +222,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
} }
initLocalizer();
}); });
</script> </script>
{{- end}} {{- end}}

View file

@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "content"}} {{define "content"}}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Branding.ShortName}} - {{.Version}}</h1> <h1 class="h4 text-gray-900 mb-4">{{.Branding.ShortName}}</h1>
</div> </div>
{{if .Error}} {{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert"> <div class="alert alert-warning alert-dismissible fade show" role="alert">

View file

@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "content"}} {{define "content"}}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}} - {{.Version}}</h1> <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}}</h1>
</div> </div>
{{if .Error}} {{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert"> <div class="alert alert-warning alert-dismissible fade show" role="alert">

View file

@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "content"}} {{define "content"}}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}} - {{.Version}}</h1> <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}}</h1>
</div> </div>
{{if .Error}} {{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert"> <div class="alert alert-warning alert-dismissible fade show" role="alert">

View file

@ -17,7 +17,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{.Branding.Name}} - {{template "title" .}}</title> <title></title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="" /> <meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@ -36,7 +36,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<body data-kt-app-header-fixed="true" data-kt-app-header-fixed-mobile="true" data-kt-app-toolbar-enabled="true" data-kt-app-sidebar-enabled="true" data-kt-app-sidebar-fixed="true" data-kt-app-sidebar-push-header="true" data-kt-app-sidebar-push-toolbar="true" data-kt-app-sidebar-push-footer="true" class="app-default"> <body data-kt-app-header-fixed="true" data-kt-app-header-fixed-mobile="true" data-kt-app-toolbar-enabled="true" data-kt-app-sidebar-enabled="true" data-kt-app-sidebar-fixed="true" data-kt-app-sidebar-push-header="true" data-kt-app-sidebar-push-toolbar="true" data-kt-app-sidebar-push-footer="true" class="app-default">
{{- template "theme-setup" .CSPNonce }} {{- template "theme-setup" .CSPNonce }}
<div class="d-flex flex-column flex-root app-root" id="kt_app_root"> <div id="app_loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
</div>
<div class="d-flex flex-column flex-root app-root d-none" id="app_root">
<div class="app-page flex-column flex-column-fluid " id="kt_app_page"> <div class="app-page flex-column flex-column-fluid " id="kt_app_page">
{{- if .LoggedUser.Username}} {{- if .LoggedUser.Username}}
<div id="kt_app_header" class="app-header" data-kt-sticky="true" data-kt-sticky-activate="{default: true, lg: true}" data-kt-sticky-name="app-header-minimize" data-kt-sticky-offset="{default: '200px', lg: '300px'}" data-kt-sticky-animation="false"> <div id="kt_app_header" class="app-header" data-kt-sticky="true" data-kt-sticky-activate="{default: true, lg: true}" data-kt-sticky-name="app-header-minimize" data-kt-sticky-offset="{default: '200px', lg: '300px'}" data-kt-sticky-animation="false">
@ -96,7 +99,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path10"></span> <span class="path10"></span>
</i> </i>
</span> </span>
<span class="menu-title">Light</span> <span data-i18n="theme.light" class="menu-title">Light</span>
</a> </a>
</div> </div>
<div class="menu-item px-3 my-0"> <div class="menu-item px-3 my-0">
@ -107,7 +110,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path2"></span> <span class="path2"></span>
</i> </i>
</span> </span>
<span class="menu-title">Dark</span> <span data-i18n="theme.dark" class="menu-title">Dark</span>
</a> </a>
</div> </div>
<div class="menu-item px-3 my-0"> <div class="menu-item px-3 my-0">
@ -120,7 +123,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path4"></span> <span class="path4"></span>
</i> </i>
</span> </span>
<span class="menu-title">System</span> <span data-i18n="theme.system" class="menu-title">System</span>
</a> </a>
</div> </div>
</div> </div>
@ -143,7 +146,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</i> </i>
</div> </div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div class="fw-bold d-flex align-items-center fs-5"> <div class="fw-semibold d-flex align-items-center fs-5">
<span class="w-175px wrap-word">{{.LoggedUser.Username}}</span> <span class="w-175px wrap-word">{{.LoggedUser.Username}}</span>
</div> </div>
</div> </div>
@ -152,19 +155,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="separator my-2"></div> <div class="separator my-2"></div>
<div class="menu-item px-3 my-0"> <div class="menu-item px-3 my-0">
<a href="{{.ProfileURL}}" class="menu-link px-3 py-2"> <a href="{{.ProfileURL}}" class="menu-link px-3 py-2">
<span class="menu-title">Profile</span> <span data-i18n="title.profile" class="menu-title">Profile</span>
</a> </a>
</div> </div>
{{- if .LoggedUser.CanChangePassword}} {{- if .LoggedUser.CanChangePassword}}
<div class="menu-item px-3 my-0"> <div class="menu-item px-3 my-0">
<a href="{{.ChangePwdURL}}" class="menu-link px-3 py-2"> <a href="{{.ChangePwdURL}}" class="menu-link px-3 py-2">
<span class="menu-title">Change password</span> <span data-i18n="title.change_password" class="menu-title">Change password</span>
</a> </a>
</div> </div>
{{- end}} {{- end}}
<div class="menu-item px-3 my-0"> <div class="menu-item px-3 my-0">
<a id="id_logout_link" href="#" class="menu-link px-3 py-2"> <a id="id_logout_link" href="#" class="menu-link px-3 py-2">
<span class="menu-title">Sign out</span> <span data-i18n="login.signout" class="menu-title">Sign out</span>
</a> </a>
</div> </div>
</div> </div>
@ -201,7 +204,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path2"></span> <span class="path2"></span>
</i> </i>
</span> </span>
<span class="menu-title">{{.FilesTitle}}</span> <span data-i18n="title.files" class="menu-title">Files</span>
</a> </a>
</div> </div>
{{- if .LoggedUser.CanManageShares}} {{- if .LoggedUser.CanManageShares}}
@ -217,7 +220,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path6"></span> <span class="path6"></span>
</i> </i>
</span> </span>
<span class="menu-title">{{.SharesTitle}}</span> <span data-i18n="title.shares" class="menu-title">Shares</span>
</a> </a>
</div> </div>
{{- end}} {{- end}}
@ -230,7 +233,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path2"></span> <span class="path2"></span>
</i> </i>
</span> </span>
<span class="menu-title">{{.MFATitle}}</span> <span data-i18n="title.two_factor_auth_short" class="menu-title">2FA</span>
</a> </a>
</div> </div>
{{- end}} {{- end}}
@ -294,7 +297,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- block "modals" .}}{{- end}} {{- block "modals" .}}{{- end}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/global/plugins.bundle.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/global/plugins.bundle.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/js/scripts.bundle.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/js/scripts.bundle.js"></script>
{{- template "basejs" .CSPNonce }} <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18next.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/jquery-i18next.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextBrowserLanguageDetector.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextChainedBackend.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextLocalStorageBackend.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextHttpBackend.js"></script>
{{- template "basejs" . }}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
var ModalAlert = function () { var ModalAlert = function () {
var modal; var modal;
@ -387,10 +396,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
function doLogout() { function doLogout() {
ModalAlert.fire({ ModalAlert.fire({
text: `Are you sure you want to sign out?`, text: $.t("general.confirm_logout"),
icon: "question", icon: "question",
confirmButtonText: "Sign out", confirmButtonText: $.t("login.signout"),
cancelButtonText: 'Cancel', cancelButtonText: $.t("general.cancel"),
customClass: { customClass: {
confirmButton: "btn btn-primary", confirmButton: "btn btn-primary",
cancelButton: 'btn btn-secondary' cancelButton: 'btn btn-secondary'

View file

@ -17,7 +17,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{.Branding.Name}} - {{template "title" .}}</title> <title></title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="" /> <meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@ -35,7 +35,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<body class="app-blank"> <body class="app-blank">
{{- template "theme-setup" .CSPNonce}} {{- template "theme-setup" .CSPNonce}}
<div class="d-flex flex-column flex-root"> <div id="app_loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
</div>
<div class="d-flex flex-column flex-root d-none" id="app_root">
<div class="d-flex flex-column flex-column-fluid bgi-position-y-bottom position-x-center bgi-no-repeat bgi-size-contain bgi-attachment-fixed"> <div class="d-flex flex-column flex-column-fluid bgi-position-y-bottom position-x-center bgi-no-repeat bgi-size-contain bgi-attachment-fixed">
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20"> <div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
<div class="w-lg-500px w-md-450px w-sm-400px bg-body rounded shadow-sm p-10 p-lg-15 mx-auto"> <div class="w-lg-500px w-md-450px w-sm-400px bg-body rounded shadow-sm p-10 p-lg-15 mx-auto">
@ -46,7 +49,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/global/plugins.bundle.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/global/plugins.bundle.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/js/scripts.bundle.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/js/scripts.bundle.js"></script>
{{- template "basejs" .CSPNonce }} <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18next.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/jquery-i18next.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextBrowserLanguageDetector.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextChainedBackend.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextLocalStorageBackend.js"></script>
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/vendor/i18next/i18nextHttpBackend.js"></script>
{{- template "basejs" . }}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
KTUtil.onDOMContentLoaded(function () { KTUtil.onDOMContentLoaded(function () {
$('#sign_in_form').submit(function (event) { $('#sign_in_form').submit(function (event) {

View file

@ -15,32 +15,45 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">Change password</h3> <h3 data-i18n="title.change_password" class="card-title text-primary">Change password</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5">
<i class="ki-duotone ki-shield-tick fs-2tx text-primary me-4">
<span class="path1"></span>
<span class="path2"></span>
</i>
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0">
<div class="fs-6 text-gray-800 fw-semibold pe-7">
<span data-i18n="change_pwd.info">
</span>
</div>
</div>
</div>
</div>
{{- template "errmsg" .Error}} {{- template "errmsg" .Error}}
<form id="change_pwd_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"> <form id="change_pwd_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row"> <div class="form-group row">
<label class="col-md-3 col-form-label required">Current password</label> <label data-i18n="change_pwd.current" class="col-md-3 col-form-label required">Current password</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" class="form-control" placeholder="" name="current_password" <input type="password" class="form-control" placeholder="" name="current_password"
spellcheck="false" required /> spellcheck="false" required />
</div> </div>
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label required">New password</label> <label data-i18n="change_pwd.new" class="col-md-3 col-form-label required">New password</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" class="form-control" placeholder="" name="new_password1" <input type="password" class="form-control" placeholder="" name="new_password1"
autocomplete="new-password" spellcheck="false" required /> autocomplete="new-password" spellcheck="false" required />
</div> </div>
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label required">Confirm password</label> <label data-i18n="change_pwd.confirm" class="col-md-3 col-form-label required">Confirm password</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" class="form-control" placeholder="" name="new_password2" <input type="password" class="form-control" placeholder="" name="new_password2"
autocomplete="new-password" spellcheck="false" required /> autocomplete="new-password" spellcheck="false" required />
@ -49,11 +62,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex justify-content-end mt-12"> <div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10"> <button type="submit" id="form_submit" class="btn btn-primary px-10">
<span class="indicator-label"> <span data-i18n="change_pwd.save" class="indicator-label">
Submit Submit
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
</div> </div>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "extra_css"}} {{- define "extra_css"}}
<style {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <style {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
.shortcut {font-family: monospace; color: #666;} .shortcut {font-family: monospace; color: #666;}
@ -44,17 +42,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- template "errmsg" ""}} {{- template "errmsg" ""}}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header"> <div class="card-header">
{{- if .ReadOnly}} <h6 id="card_title" class="card-title"></h6>
<h6 class="card-title">View file "{{.Path}}"</h6>
{{- else}}
<h6 class="card-title">Edit file "{{.Path}}"</h6>
{{- end}}
<div class="card-toolbar"> <div class="card-toolbar">
<a class="btn btn-light-primary px-10 me-5" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a> <a data-i18n="general.back" class="btn btn-light-primary px-10 me-5" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
{{- if not .ReadOnly}} {{- if not .ReadOnly}}
<a id="save_button" type="button" class="btn btn-primary px-10" href="#" role="button"> <a id="save_button" type="button" class="btn btn-primary px-10" href="#" role="button">
<span class="indicator-label">Save</span> <span data-i18n="general.submit" class="indicator-label">Save</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</a> </a>
@ -72,27 +67,27 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">Editor keybindings</h3> <h3 data-i18n="editor.keybinding" class="modal-title">Editor keybindings</h3>
<div class="btn btn-icon btn-sm btn-active-light-primary ms-2" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-light-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p> <p>
<span class="shortcut">Ctrl-F / Cmd-F</span> => Open search panel <span class="shortcut">Ctrl-F / Cmd-F</span> => <span data-i18n="editor.search" class="fw-semibold">Open search panel</span>
</p> </p>
<p> <p>
<span class="shortcut">Alt-G</span> => Jump to line <span class="shortcut">Alt-G</span> => <span data-i18n="editor.goto" class="fw-semibold">Jump to line</span>
</p> </p>
<p> <p>
<span class="shortcut">Tab</span> => Indent more <span class="shortcut">Tab</span> => <span data-i18n="editor.indent_more" class="fw-semibold">Indent more</span>
</p> </p>
<p> <p>
<span class="shortcut">Shift-Tab</span> => Indent less <span class="shortcut">Shift-Tab</span> => <span data-i18n="editor.indent_less" class="fw-semibold">Indent less</span>
</p> </p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button> <button data-i18n="general.ok" class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
</div> </div>
</div> </div>
</div> </div>
@ -134,22 +129,36 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function (error) { }).catch(function (error) {
saveButton.removeAttribute('data-kt-indicator'); saveButton.removeAttribute('data-kt-indicator');
saveButton.disabled = false; saveButton.disabled = false;
let errorMessage = "Error saving file";
let errorMessage = "";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.save.err403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.save.err429";
break;
} }
} }
$('#errorTxt').text(errorMessage); if (!errorMessage){
errorMessage = "fs.save.err_generic";
}
setI18NData($('#errorTxt'), errorMessage);
$('#errorMsg').removeClass("d-none"); $('#errorMsg').removeClass("d-none");
}); });
} }
//{{- end}} //{{- end}}
KTUtil.onDOMContentLoaded(function () { $(document).on("i18nload", function(){
//{{- if .ReadOnly}}
$('#card_title').text($.t('fs.view_file', { path: '{{.Path}}'}));
//{{- else}}
$('#card_title').text($.t('fs.edit_file', { path: '{{.Path}}'}));
//{{- end}}
});
$(document).on("i18nshow", function(){
let filename = "{{.Name}}"; let filename = "{{.Name}}";
let extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase(); let extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
let options = { let options = {

View file

@ -15,7 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "page_body"}} {{- define "page_body"}}
{{- template "errmsg" .Error}} {{- template "errmsg" .Error}}
<div class="card card-flush shadow-sm"> <div class="card card-flush shadow-sm">
@ -26,13 +25,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
</i> </i>
<input type="text" data-kt-filemanager-table-filter="search" class="form-control form-control-solid w-250px ps-15" placeholder="Search Files & Folders" /> <input data-i18n="[placeholder]general.search" type="text" data-kt-filemanager-table-filter="search" class="form-control form-control-solid w-250px ps-15" placeholder="Search Files & Folders" />
</div> </div>
</div> </div>
<div class="card-toolbar"> <div class="card-toolbar">
<div class="d-flex justify-content-end" data-kt-filemanager-table-toolbar="base"> <div class="d-flex justify-content-end" data-kt-filemanager-table-toolbar="base">
{{- if .CanCreateDirs}} {{- if .CanCreateDirs}}
<button id="id_create_dir_button" type="button" class="btn btn-flex btn-light-primary me-3"> <button id="id_create_dir_button" data-i18n="fs.new_folder" type="button" class="btn btn-flex btn-light-primary me-3">
<i class="ki-duotone ki-add-folder fs-2"> <i class="ki-duotone ki-add-folder fs-2">
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
@ -41,7 +40,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</button> </button>
{{- end}} {{- end}}
{{- if .CanAddFiles}} {{- if .CanAddFiles}}
<button type="button" class="btn btn-flex btn-primary" data-bs-toggle="modal" data-bs-target="#modal_upload"> <button type="button" data-i18n="fs.upload.text" class="btn btn-flex btn-primary" data-bs-toggle="modal" data-bs-target="#modal_upload">
<i class="ki-duotone ki-folder-up fs-2"> <i class="ki-duotone ki-folder-up fs-2">
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
@ -56,20 +55,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-check form-switch form-check-custom form-check-solid me-5" data-kt-filemanager-table-select="select_all_pages_container"> <div class="form-check form-switch form-check-custom form-check-solid me-5" data-kt-filemanager-table-select="select_all_pages_container">
<input class="form-check-input" type="checkbox" id="id_select_all_pages" data-kt-filemanager-table-select="select_all_pages" /> <input class="form-check-input" type="checkbox" id="id_select_all_pages" data-kt-filemanager-table-select="select_all_pages" />
<label class="form-check-label fw-semibold text-gray-900" for="id_select_all_pages"> <label data-i18n="fs.select_across_pages" class="form-check-label fw-semibold text-gray-900" for="id_select_all_pages">
Select across pages Select across pages
</label> </label>
</div> </div>
{{- if or .CanDownload .CanDelete}} {{- if or .CanDownload .CanDelete}}
<div> <div>
<button type="button" class="btn btn-light-primary rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom"> <button data-i18n="fs.actions" type="button" class="btn btn-light-primary rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom">
Actions Actions
<i class="ki-duotone ki-down fs-3 rotate-180 ms-3 me-0"></i> <i class="ki-duotone ki-down fs-3 rotate-180 ms-3 me-0"></i>
</button> </button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4" data-kt-menu="true"> <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4" data-kt-menu="true">
{{- if .CanDownload}} {{- if .CanDownload}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="download_selected"> <a data-i18n="fs.download" href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="download_selected">
Download Download
</a> </a>
</div> </div>
@ -77,14 +76,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- if not .ShareUploadBaseURL}} {{- if not .ShareUploadBaseURL}}
{{- if or .CanRename .CanAddFiles}} {{- if or .CanRename .CanAddFiles}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="move_or_copy_selected"> <a data-i18n="fs.move_copy" href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="move_or_copy_selected">
Move or copy Move or copy
</a> </a>
</div> </div>
{{- end}} {{- end}}
{{- if .CanShare}} {{- if .CanShare}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="share_selected"> <a data-i18n="fs.share" href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="share_selected">
Share Share
</a> </a>
</div> </div>
@ -92,7 +91,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- end}} {{- end}}
{{- if .CanDelete}} {{- if .CanDelete}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3 text-danger fs-6" data-kt-filemanager-table-select="delete_selected"> <a data-i18n="general.delete" href="#" class="menu-link px-3 text-danger fs-6" data-kt-filemanager-table-select="delete_selected">
Delete Delete
</a> </a>
</div> </div>
@ -108,7 +107,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="badge badge-lg badge-light-primary"> <div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<i class="ki-duotone ki-home fs-1 text-primary me-3"></i> <i class="ki-duotone ki-home fs-1 text-primary me-3"></i>
<a href="{{.FilesURL}}?path=%2F">Home</a> <a data-i18n="fs.home" href="{{.FilesURL}}?path=%2F">Home</a>
{{- range .Paths}} {{- range .Paths}}
<i class="ki-duotone ki-right fs-2x text-primary mx-1"></i> <i class="ki-duotone ki-right fs-2x text-primary mx-1"></i>
{{- if eq .Href ""}} {{- if eq .Href ""}}
@ -127,7 +126,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path2"></span> <span class="path2"></span>
</i> </i>
</span> </span>
<input id="file_manager_new_folder_input" type="text" name="new_folder_name" placeholder="Enter the new folder name" class="form-control mw-250px me-3" /> <input data-i18n="[placeholder]fs.create_folder_msg" id="file_manager_new_folder_input" type="text" name="new_folder_name" placeholder="Enter the new folder name" class="form-control mw-250px me-3" />
<button class="btn btn-icon btn-light-primary me-3" id="file_manager_add_folder"> <button class="btn btn-icon btn-light-primary me-3" id="file_manager_add_folder">
<span class="indicator-label"> <span class="indicator-label">
<i class="ki-duotone ki-check fs-1"></i> <i class="ki-duotone ki-check fs-1"></i>
@ -152,9 +151,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</th> </th>
<th></th> <th></th>
<th class="min-w-250px">Name</th> <th data-i18n="general.name" class="min-w-250px">Name</th>
<th class="min-w-10px">Size</th> <th data-i18n="general.size" class="min-w-10px">Size</th>
<th class="min-w-125px">Last Modified</th> <th data-i18n="general.last_modified" class="min-w-125px">Last Modified</th>
<th class="w-125px"></th> <th class="w-125px"></th>
</tr> </tr>
</thead> </thead>
@ -224,18 +223,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
dataSrc: "", dataSrc: "",
error: function ($xhr, textStatus, errorThrown) { error: function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide(); $(".dataTables_processing").hide();
let txt = "Failed to get directory listing"; let txt = "";
if ($xhr) { if ($xhr) {
let json = $xhr.responseJSON; let json = $xhr.responseJSON;
if (json) { if (json) {
if (json.message) { if (json.message) {
txt += ": " + json.message; txt = json.message;
} else {
txt += ": " + json.error;
} }
} }
} }
$('#errorModalTxt').text(txt); if (!txt){
txt = "fs.dir_list.err_generic";
}
setI18NData($('#errorModalTxt'), txt);
$('#errorModalMsg').removeClass("d-none"); $('#errorModalMsg').removeClass("d-none");
} }
}, },
@ -277,8 +277,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
], ],
language: { language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "", loadingRecords: "",
emptyTable: "No more subfolders in here" processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('fs.no_more_subfolders')
}, },
order: [1, 'asc'] order: [1, 'asc']
}); });
@ -289,7 +294,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let mainNavIcon = document.createElement("i"); let mainNavIcon = document.createElement("i");
mainNavIcon.classList.add("ki-duotone", "ki-home", "fs-1", "text-primary", "me-3"); mainNavIcon.classList.add("ki-duotone", "ki-home", "fs-1", "text-primary", "me-3");
let mainNavLink = document.createElement("a"); let mainNavLink = document.createElement("a");
mainNavLink.textContent = "Home"; mainNavLink.textContent = $.t('fs.home');
mainNavLink.href = "#"; mainNavLink.href = "#";
mainNavLink.addEventListener("click", function(e){ mainNavLink.addEventListener("click", function(e){
e.preventDefault(); e.preventDefault();
@ -342,12 +347,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let cancelButton = document.querySelector('#dirsbrowser_cancel_folder'); let cancelButton = document.querySelector('#dirsbrowser_cancel_folder');
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
if (!dirName){ if (!dirName){
errTxtEl.text("Folder name is required"); setI18NData(errTxtEl, "fs.folder_name_required");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
if (dirName.includes("/")){ if (dirName.includes("/")){
errTxtEl.text('"/" is not allowed in file or directory names'); setI18NData(errTxtEl, "fs.invalid_name");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -372,16 +377,21 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
cancelButton.disabled = false; cancelButton.disabled = false;
$('#dirsbrowser_new_folder').addClass("d-none"); $('#dirsbrowser_new_folder').addClass("d-none");
}).catch(function (error) { }).catch(function (error) {
let errorMessage = "Unable to create the new folder"; let errorMessage = "";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.create_dir.err_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.create_dir.err_429";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "fs.create_dir.err_generic";
}
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
submitButton.removeAttribute('data-kt-indicator'); submitButton.removeAttribute('data-kt-indicator');
submitButton.disabled = false; submitButton.disabled = false;
@ -434,18 +444,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
dataSrc: "", dataSrc: "",
error: function ($xhr, textStatus, errorThrown) { error: function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide(); $(".dataTables_processing").hide();
let txt = "Failed to get directory listing"; let txt = "";
if ($xhr) { if ($xhr) {
let json = $xhr.responseJSON; let json = $xhr.responseJSON;
if (json) { if (json) {
if (json.message){ if (json.message){
txt += ": " + json.message; txt = json.message;
} else {
txt += ": " + json.error;
} }
} }
} }
$('#errorTxt').text(txt); if (!txt){
txt = "fs.dir_list.err_generic";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none"); $('#errorMsg').removeClass("d-none");
} }
}, },
@ -522,7 +533,23 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
return data; return data;
} }
}, },
{ data: "last_modified" }, {
data: "last_modified",
render: function (data, type, row) {
if (type === 'display') {
if (data){
return $.t('general.datetime', {
val: new Date(data),
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },
}
});
}
return ""
}
return data;
}
},
{ {
data: "edit_url", data: "edit_url",
className: 'text-end', className: 'text-end',
@ -614,22 +641,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-7 w-150px py-4" data-kt-menu="true"> <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-7 w-150px py-4" data-kt-menu="true">
{{- if .CanRename}} {{- if .CanRename}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-kt-filemanager-table-action="rename">Rename</a> <a data-i18n="general.rename" href="#" class="menu-link px-3" data-kt-filemanager-table-action="rename">Rename</a>
</div> </div>
{{- end}} {{- end}}
{{- if or .CanRename .CanAddFiles}} {{- if or .CanRename .CanAddFiles}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-kt-filemanager-table-action="move_or_copy">Move or copy</a> <a data-i18n="fs.move_copy" href="#" class="menu-link px-3" data-kt-filemanager-table-action="move_or_copy">Move or copy</a>
</div> </div>
{{- end}} {{- end}}
{{- if .CanShare}} {{- if .CanShare}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-kt-filemanager-table-action="share">Share</a> <a data-i18n="fs.share" href="#" class="menu-link px-3" data-kt-filemanager-table-action="share">Share</a>
</div> </div>
{{- end}} {{- end}}
{{- if .CanDelete}} {{- if .CanDelete}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link text-danger px-3" data-kt-filemanager-table-action="delete">Delete</a> <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-kt-filemanager-table-action="delete">Delete</a>
</div> </div>
{{- end}} {{- end}}
</div> </div>
@ -674,8 +701,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
], ],
language: { language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "", loadingRecords: "",
emptyTable: "No files or folders" processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('fs.no_files_folders')
}, },
orderFixed: [1, 'asc'], orderFixed: [1, 'asc'],
order: [2, 'asc'] order: [2, 'asc']
@ -706,6 +738,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
toggleToolbars(); toggleToolbars();
handleRowActions(); handleRowActions();
$('#file_manager_list_body').localize();
}); });
dt.on('user-select', function(e, dt, type, cell, originalEvent){ dt.on('user-select', function(e, dt, type, cell, originalEvent){
@ -754,7 +787,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
selectAllContainer.classList.add('d-none'); selectAllContainer.classList.add('d-none');
} }
if (selectedCount){ if (selectedCount){
selectedCount.innerHTML = `${totalSelected} Selected`; selectedCount.innerHTML = $.t('general.selected_items', { count: totalSelected});
} }
if (toolbarBase){ if (toolbarBase){
toolbarBase.classList.add('d-none'); toolbarBase.classList.add('d-none');
@ -926,10 +959,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (deleteButton) { if (deleteButton) {
deleteButton.addEventListener('click', function(e){ deleteButton.addEventListener('click', function(e){
ModalAlert.fire({ ModalAlert.fire({
text: "Do you want to delete the selected item/s? This action is irreversible", text: $.t('general.delete_multi_confirm'),
icon: "warning", icon: "warning",
confirmButtonText: "Delete", confirmButtonText: $.t('general.delete'),
cancelButtonText: 'Cancel', cancelButtonText: $.t('general.cancel'),
customClass: { customClass: {
confirmButton: "btn btn-danger", confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary' cancelButton: 'btn btn-secondary'
@ -967,8 +1000,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let attrs = getDeleteReqAttrs(meta); let attrs = getDeleteReqAttrs(meta);
let deleteTxt = ""; let deleteTxt = "";
if (selectedRowsIdx.length > 1){ if (selectedRowsIdx.length > 1){
let name = getNameFromMeta(meta); deleteTxt = $.t('fs.deleting', {
deleteTxt = `Delete ${index+1}/${selectedRowsIdx.length}: ${name}`; idx : index + 1,
total: selectedRowsIdx.length,
name: getNameFromMeta(meta)
});
} }
$('#loading_message').text(deleteTxt); $('#loading_message').text(deleteTxt);
axios.delete(attrs.path,{ axios.delete(attrs.path,{
@ -986,19 +1022,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function(error){ }).catch(function(error){
index++; index++;
hasError = true; hasError = true;
let errorMessage = "Unable to delete the selected item/s"; let errorMessage;
if (deleted > 0){
errorMessage = "Not all the selected items have been deleted, please reload the page";
}
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
if (deleted > 0){
errorMessage = "fs.delete_multi.err_403_partial";
} else {
errorMessage = "fs.delete_multi.err_403";
} }
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
if (deleted > 0){
errorMessage = "fs.delete_multi.err_429_partial";
} else {
errorMessage = "fs.delete_multi.err_429";
}
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
if (deleted > 0){
errorMessage = "fs.delete_multi.err_generic_partial";
} else {
errorMessage = "fs.delete_multi.err_generic";
}
}
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
deleteSelected(); deleteSelected();
}); });
@ -1122,7 +1172,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
items = checkMoveCopyItems(items) items = checkMoveCopyItems(items)
if (items.length == 0){ if (items.length == 0){
errTxtEl.text('"/" is not allowed in file or directory names'); setI18NData(errTxtEl, "fs.invalid_name");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -1156,9 +1206,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
targetPath+=item.targetName; targetPath+=item.targetName;
if (items.length > 1){ if (items.length > 1) {
let msgTxt = `${sourcePath} => ${targetPath}`; msgTxt = $.t('fs.copying', {
msgTxt = `Copy ${index+1}/${items.length}: ${msgTxt}`; idx: index + 1,
total: items.length,
name: `${sourcePath} => ${targetPath}`
});
$('#loading_message').text(msgTxt); $('#loading_message').text(msgTxt);
} }
let path = '{{.FileActionsURL}}/copy'; let path = '{{.FileActionsURL}}/copy';
@ -1178,16 +1231,21 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function (error) { }).catch(function (error) {
index++; index++;
hasError = true; hasError = true;
let errorMessage = "Error copying item"; let errorMessage = "";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.copy.err_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.copy.err_429";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "fs.copy.err_generic";
}
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
copyItem(); copyItem();
}); });
@ -1206,7 +1264,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
items = checkMoveCopyItems(items) items = checkMoveCopyItems(items)
if (items.length == 0){ if (items.length == 0){
errTxtEl.text('"/" is not allowed in file or directory names'); setI18NData(errTxtEl, "fs.invalid_name");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -1240,9 +1298,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
targetPath+=item.targetName; targetPath+=item.targetName;
if (items.length > 1){ if (items.length > 1) {
let msgTxt = `${sourcePath} => ${targetPath}`; msgTxt = $.t('fs.moving', {
msgTxt = `Move ${index+1}/${items.length}: ${msgTxt}`; idx: index + 1,
total: items.length,
name: `${sourcePath} => ${targetPath}`
});
$('#loading_message').text(msgTxt); $('#loading_message').text(msgTxt);
} }
let path = '{{.FileActionsURL}}/move'; let path = '{{.FileActionsURL}}/move';
@ -1262,16 +1323,21 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function (error) { }).catch(function (error) {
index++; index++;
hasError = true; hasError = true;
let errorMessage = "Error moving item"; let errorMessage = "";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.move.err_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.move.err_429";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "fs.move.err_generic";
}
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
moveItem(); moveItem();
}); });
@ -1303,10 +1369,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let itemName = getNameFromMeta(meta); let itemName = getNameFromMeta(meta);
ModalAlert.fire({ ModalAlert.fire({
text: `Do you want to delete "${itemName}"? This action is irreversible`, text: $.t('general.delete_confirm', {name: itemName}),
icon: "warning", icon: "warning",
confirmButtonText: "Delete", confirmButtonText: $.t('general.delete'),
cancelButtonText: 'Cancel', cancelButtonText: $.t('general.cancel'),
customClass: { customClass: {
confirmButton: "btn btn-danger", confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary' cancelButton: 'btn btn-secondary'
@ -1329,16 +1395,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
location.reload(); location.reload();
}).catch(function(error){ }).catch(function(error){
KTApp.hidePageLoading(); KTApp.hidePageLoading();
let errorMessage = `Unable to delete "${itemName}"`; let errorMessage;
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.delete.err_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.delete.err_429";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "fs.delete.err_generic";
}
errTxtEl.removeAttr("data-i18n")
errTxtEl.text($.t(errorMessage, {name: itemName}));
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }
@ -1371,17 +1443,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let errDivEl = $('#errorMsg'); let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt'); let errTxtEl = $('#errorTxt');
if (!newName){ if (!newName){
errTxtEl.text("New name is required"); setI18NData(errTxtEl, "general.name_required");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
if (newName == oldName){ if (newName == oldName){
errTxtEl.text("The new name must be different from the current name"); setI18NData(errTxtEl, "general.name_different");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
if (newName.includes("/")){ if (newName.includes("/")){
errTxtEl.text('"/" is not allowed in file or directory names'); setI18NData(errTxtEl, "fs.invalid_name");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -1398,16 +1470,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then(function (response) { }).then(function (response) {
location.reload(); location.reload();
}).catch(function (error) { }).catch(function (error) {
let errorMessage = `Unable to rename "${oldName}"`; let errorMessage;
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.rename.err_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.rename.err_429";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "fs.rename.err_generic";
}
errTxtEl.removeAttr("data-i18n")
errTxtEl.text($.t(errorMessage, {name: oldName}));
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }
@ -1444,12 +1522,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let cancelButton = document.querySelector('#file_manager_cancel_folder'); let cancelButton = document.querySelector('#file_manager_cancel_folder');
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
if (!dirName){ if (!dirName){
errTxtEl.text("Folder name is required"); setI18NData(errTxtEl, "fs.folder_name_required");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
if (dirName.includes("/")){ if (dirName.includes("/")){
errTxtEl.text('"/" is not allowed in file or directory names'); setI18NData(errTxtEl, "fs.invalid_name");
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -1470,16 +1548,21 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then(function (response) { }).then(function (response) {
location.reload(); location.reload();
}).catch(function (error) { }).catch(function (error) {
let errorMessage = "Unable to create the new folder"; let errorMessage = "";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "fs.create_dir.err_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 429:
errorMessage = "fs.create_dir.err_429";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "fs.create_dir.err_generic";
}
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
submitButton.removeAttribute('data-kt-indicator'); submitButton.removeAttribute('data-kt-indicator');
submitButton.disabled = false; submitButton.disabled = false;
@ -1523,13 +1606,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
try { try {
lastModified = f.lastModified; lastModified = f.lastModified;
} catch (e) { } catch (e) {
console.log("unable to get last modified time from file: " + e.message); console.error("unable to get last modified time from file: " + e.message);
lastModified = ""; lastModified = "";
} }
let uploadTxt = f.name; let uploadTxt = f.name;
if (files.length > 1){ if (files.length > 1){
uploadTxt = `Upload ${index+1}/${files.length}: ${uploadTxt}`; uploadTxt = $.t('fs.uploading', {
idx: index + 1,
total: files.length,
name: uploadTxt
});
} }
$('#loading_message').text(uploadTxt); $('#loading_message').text(uploadTxt);
@ -1556,18 +1643,23 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
success++; success++;
uploadFile(); uploadFile();
}).catch(function (error) { }).catch(function (error) {
let errorMessage = "Error uploading files"; let errorMessage;
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
errorMessage = "fs.upload.err_403";
break;
case 429:
errorMessage = "fs.upload.err_429";
break;
} }
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
} }
if (!errorMessage){
errorMessage = "fs.upload.err_generic";
} }
index++; index++;
has_errors = true; has_errors = true;
$('#errorTxt').text(errorMessage); setI18NData($('#errorTxt'), errorMessage);
$('#errorMsg').removeClass("d-none"); $('#errorMsg').removeClass("d-none");
uploadFile(); uploadFile();
}); });
@ -1585,8 +1677,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
playerKeepAlive = setInterval(keepAlive, 300000); playerKeepAlive = setInterval(keepAlive, 300000);
} }
// On document ready $(document).on("i18nshow", function(){
KTUtil.onDOMContentLoaded(function () {
KTDatatablesServerSide.init(); KTDatatablesServerSide.init();
var dropzone = new Dropzone("#upload_files", { var dropzone = new Dropzone("#upload_files", {
@ -1704,10 +1795,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path3"></span> <span class="path3"></span>
</i> </i>
</div> </div>
<div class="menu menu-sub menu-sub-dropdown menu-column w-350px" data-kt-menu="true"> <div class="menu menu-sub menu-sub-dropdown menu-column w-375px" data-kt-menu="true">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"><span class="text-gray-700 fw-semibold fs-6">Quota usage</span></h3> <h3 class="card-title"><span data-i18n="fs.quota_usage.title" class="text-gray-700 fw-bold fs-6">Quota usage</span></h3>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
{{- if .QuotaUsage.HasDiskQuota}} {{- if .QuotaUsage.HasDiskQuota}}
@ -1726,14 +1817,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</span> </span>
</div> </div>
<div class="mb-1 pe-3 flex-grow-1"> <div class="mb-1 pe-3 flex-grow-1">
<span class="fs-6 text-gray-900 fw-semibold">Disk quota</span> <span data-i18n="fs.quota_usage.disk" class="fs-6 text-gray-900 fw-semibold">Disk quota</span>
{{- if $size}} {{- if $size}}
{{- $percentage := .QuotaUsage.GetQuotaSizePercentage}} {{- $percentage := .QuotaUsage.GetQuotaSizePercentage}}
<div class="{{if .QuotaUsage.IsQuotaSizeLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">Size: {{$size}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</div> <div class="{{if .QuotaUsage.IsQuotaSizeLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">
<span {{if gt $percentage 0}}data-i18n="fs.quota_usage.size_percentage" data-i18n-options='{ "val": "{{$size}}", "percentage": {{$percentage}} }'{{else}}data-i18n="fs.quota_usage.size" data-i18n-options='{ "val": "{{$size}}" }'{{end}}></span>
</div>
{{- end}} {{- end}}
{{- if $files}} {{- if $files}}
{{- $percentage := .QuotaUsage.GetQuotaFilesPercentage}} {{- $percentage := .QuotaUsage.GetQuotaFilesPercentage}}
<div class="{{if .QuotaUsage.IsQuotaFilesLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">Files: {{$files}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</div> <div class="{{if .QuotaUsage.IsQuotaFilesLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">
<span {{if gt $percentage 0}}data-i18n="fs.quota_usage.files_percentage" data-i18n-options='{ "val": "{{$files}}", "percentage": {{$percentage}} }'{{else}}data-i18n="fs.quota_usage.files" data-i18n-options='{ "val": "{{$files}}" }'{{end}}></span>
</div>
{{- end}} {{- end}}
</div> </div>
</div> </div>
@ -1752,18 +1847,27 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</span> </span>
</div> </div>
<div class="mb-1 pe-3 flex-grow-1"> <div class="mb-1 pe-3 flex-grow-1">
<span class="fs-6 text-gray-900 fw-semibold">Transfer quota</span> <span data-i18n="fs.quota_usage.transfer" class="fs-6 text-gray-900 fw-semibold">Transfer quota</span>
{{- if $total}} {{- if $total}}
{{$percentage := .QuotaUsage.GetTotalTransferQuotaPercentage}} {{$percentage := .QuotaUsage.GetTotalTransferQuotaPercentage}}
<div class="{{if .QuotaUsage.IsTotalTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">Total: {{$total}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</div> <div class="{{if .QuotaUsage.IsTotalTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">
<span {{if gt $percentage 0}}data-i18n="fs.quota_usage.total_percentage" data-i18n-options='{ "val": "{{$total}}", "percentage": {{$percentage}} }'{{else}}data-i18n="fs.quota_usage.total" data-i18n-options='{ "val": "{{$total}}" }'{{end}}>
</span>
</div>
{{- end}} {{- end}}
{{- if $download}} {{- if $download}}
{{$percentage := .QuotaUsage.GetDownloadTransferQuotaPercentage}} {{$percentage := .QuotaUsage.GetDownloadTransferQuotaPercentage}}
<div class="{{if .QuotaUsage.IsDownloadTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">Download: {{$download}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</div> <div class="{{if .QuotaUsage.IsDownloadTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">
<span {{if gt $percentage 0}}data-i18n="fs.quota_usage.downloads_percentage" data-i18n-options='{ "val": "{{$download}}", "percentage": {{$percentage}} }'{{else}}data-i18n="fs.quota_usage.downloads" data-i18n-options='{ "val": "{{$download}}" }'{{end}}>
</span>
</div>
{{- end}} {{- end}}
{{- if $upload}} {{- if $upload}}
{{$percentage := .QuotaUsage.GetUploadTransferQuotaPercentage}} {{$percentage := .QuotaUsage.GetUploadTransferQuotaPercentage}}
<div class="{{if .QuotaUsage.IsUploadTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">Upload: {{$upload}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</div> <div class="{{if .QuotaUsage.IsUploadTransferQuotaLow}}text-warning{{else}}text-gray-700{{end}} fw-semibold fs-6">
<span {{if gt $percentage 0}}data-i18n="fs.quota_usage.uploads_percentage" data-i18n-options='{ "val": "{{$upload}}", "percentage": {{$percentage}} }'{{else}}data-i18n="fs.quota_usage.uploads" data-i18n-options='{ "val": "{{$upload}}" }'{{end}}>
</span>
</div>
{{- end}} {{- end}}
</div> </div>
</div> </div>
@ -1780,8 +1884,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-dialog modal-dialog-centered mw-600px"> <div class="modal-dialog modal-dialog-centered mw-600px">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header border-0"> <div class="modal-header border-0">
<h3 class="modal-title">Upload files</h3> <h3 data-i18n="fs.upload.text" class="modal-title">Upload files</h3>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
@ -1792,7 +1896,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="dz-message needsclick align-items-center"> <div class="dz-message needsclick align-items-center">
<i class="ki-duotone ki-file-up fs-3x text-primary"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-file-up fs-3x text-primary"><span class="path1"></span><span class="path2"></span></i>
<div class="ms-4"> <div class="ms-4">
<h3 class="fs-5 fw-bold text-gray-900 mb-1">Drop files here or click to upload.</h3> <h3 data-i18n="fs.upload.message" class="fs-5 fw-bold text-gray-900 mb-1">Drop files here or click to upload.</h3>
<!-- <span class="fs-7 fw-semibold text-gray-500">Upload up to 30 files</span> --> <!-- <span class="fs-7 fw-semibold text-gray-500">Upload up to 30 files</span> -->
</div> </div>
</div> </div>
@ -1801,8 +1905,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</form> </form>
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
<button type="button" class="btn btn-light me-5" data-bs-dismiss="modal">Cancel</button> <button data-i18n="general.cancel" type="button" class="btn btn-light me-5" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="upload_files_button" class="btn btn-primary" data-bs-dismiss="modal">Submit</button> <button data-i18n="general.submit" type="button" id="upload_files_button" class="btn btn-primary" data-bs-dismiss="modal">Submit</button>
</div> </div>
</div> </div>
</div> </div>
@ -1815,14 +1919,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<h5 class="modal-title"> <h5 class="modal-title">
<span id="video_title"></span> <span id="video_title"></span>
</h5> </h5>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<video id="video_player" width="100%" height="auto" controls preload="metadata"> <video id="video_player" width="100%" height="auto" controls preload="metadata">
Your browser does not support HTML5 video. <span data-i18n="general.html5_media_not_supported"></span>
</video> </video>
</div> </div>
</div> </div>
@ -1837,7 +1941,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<h5 class="modal-title"> <h5 class="modal-title">
<span id="rename_title"></span> <span id="rename_title"></span>
</h5> </h5>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
@ -1845,14 +1949,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-body"> <div class="modal-body">
<input id="rename_old_name" type="text" class="d-none"/> <input id="rename_old_name" type="text" class="d-none"/>
<div class="mb-10"> <div class="mb-10">
<label for="rename_new_name" class="form-label">New name</label> <label data-i18n="fs.rename.new_name" for="rename_new_name" class="form-label"></label>
<input id="rename_new_name" type="text" class="form-control"/> <input id="rename_new_name" type="text" class="form-control"/>
</div> </div>
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
<button type="button" class="btn btn-secondary me-5" data-bs-dismiss="modal">Cancel</button> <button data-i18n="general.cancel" type="button" class="btn btn-secondary me-5" data-bs-dismiss="modal">Cancel</button>
<button id="id_do_rename_button" type="button" class="btn btn-primary" data-bs-dismiss="modal">Submit</button> <button data-i18n="general.submit" id="id_do_rename_button" type="button" class="btn btn-primary" data-bs-dismiss="modal">Submit</button>
</div> </div>
</div> </div>
</div> </div>
@ -1862,17 +1966,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-dialog modal-dialog-centered modal-lg"> <div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header border-0"> <div class="modal-header border-0">
<h3 class="modal-title"> <h3 data-i18n="general.choose_target_folder" class="modal-title">
Choose target folder Choose target folder
</h3> </h3>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="errorModalMsg" class="d-none alert alert-dismissible bg-light-warning d-flex align-items-center p-5 mb-10"> <div id="errorModalMsg" class="d-none rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10">
<i class="ki-duotone ki-information-5 fs-3x text-warning me-5"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i> <i class="ki-duotone ki-information fs-3x text-warning me-5"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i>
<div class="text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10"> <div class="text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10">
<span id="errorModalTxt"></span> <span id="errorModalTxt"></span>
</div> </div>
@ -1893,7 +1997,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
</i> </i>
Add <span data-i18n="general.add"></span>
</button> </button>
</div> </div>
</div> </div>
@ -1904,7 +2008,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path2"></span> <span class="path2"></span>
</i> </i>
</span> </span>
<input id="dirsbrowser_new_folder_input" type="text" name="new_folder_name" placeholder="Enter the new folder name" class="form-control mw-250px me-3" /> <input data-i18n="[placeholder]fs.create_folder_msg" id="dirsbrowser_new_folder_input" type="text" name="new_folder_name" placeholder="Enter the new folder name" class="form-control mw-250px me-3" />
<button class="btn btn-icon btn-light-primary me-3" id="dirsbrowser_add_folder"> <button class="btn btn-icon btn-light-primary me-3" id="dirsbrowser_add_folder">
<span class="indicator-label"> <span class="indicator-label">
<i class="ki-duotone ki-check fs-1"></i> <i class="ki-duotone ki-check fs-1"></i>
@ -1925,7 +2029,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<thead> <thead>
<tr class="text-start text-muted fw-bold fs-7 text-uppercase gs-0"> <tr class="text-start text-muted fw-bold fs-7 text-uppercase gs-0">
<th></th> <th></th>
<th class="min-w-250px">Name</th> <th data-i18n="general.name" class="min-w-250px">Name</th>
</tr> </tr>
</thead> </thead>
<tbody id="dirsbrowser_list_body" class="text-gray-600 fw-semibold"> <tbody id="dirsbrowser_list_body" class="text-gray-600 fw-semibold">
@ -1934,26 +2038,26 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="form-floating d-none"> <div class="form-floating d-none">
<input type="text" class="form-control form-control-solid" id="move_copy_source"/> <input type="text" class="form-control form-control-solid" id="move_copy_source"/>
<label for="move_copy_source">Source name</label> <label data-i18n="general.source_name" for="move_copy_source">Source name</label>
</div> </div>
<div class="form-floating mb-5 mt-7"> <div class="form-floating mb-5 mt-7">
<input type="text" class="form-control form-control-solid" id="move_copy_folder"/> <input type="text" class="form-control form-control-solid" id="move_copy_folder"/>
<label for="move_copy_folder">Target folder</label> <label data-i18n="general.target_folder" for="move_copy_folder">Target folder</label>
</div> </div>
<div class="form-floating" id="move_copy_name_container"> <div class="form-floating" id="move_copy_name_container">
<input type="text" class="form-control form-control-solid" id="move_copy_name"/> <input type="text" class="form-control form-control-solid" id="move_copy_name"/>
<label for="move_copy_name">Destination name</label> <label data-i18n="general.dest_name" for="move_copy_name">Destination name</label>
</div> </div>
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
{{- if .CanAddFiles }} {{- if .CanAddFiles }}
<button id="id_copy_button" type="button" class="btn btn-light-primary me-5" data-bs-dismiss="modal">Copy</button> <button data-i18n="fs.copy.msg" id="id_copy_button" type="button" class="btn btn-light-primary me-5" data-bs-dismiss="modal">Copy</button>
{{- end}} {{- end}}
{{- if .CanRename }} {{- if .CanRename }}
<button id="id_move_button" type="button" class="btn btn-primary" data-bs-dismiss="modal">Move</button> <button data-i18n="fs.move.msg" id="id_move_button" type="button" class="btn btn-primary" data-bs-dismiss="modal">Move</button>
{{- end}} {{- end}}
</div> </div>
</div> </div>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "baselogin" .}} {{- template "baselogin" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "content"}} {{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST"> <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10"> <div class="container mb-10">
@ -34,22 +32,25 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
<div class="text-center mb-10"> <div class="text-center mb-10">
<h2 class="text-gray-900 mb-3"> <h2 data-i18n="login.forgot_password" class="text-gray-900 mb-3">
Forgot Password ? Forgot Password ?
</h2> </h2>
<div class="text-gray-600 fw-semibold fs-4"> <div class="text-gray-600 fw-semibold fs-4">
<span data-i18n="login.forgot_password_msg">
Enter your account username below, you will receive a password reset code by email. Enter your account username below, you will receive a password reset code by email.
</span>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" placeholder="Your username" name="username" spellcheck="false" required /> <input data-i18n="[placeholder]login.your_username" class="form-control form-control-lg form-control-solid" type="text" placeholder="Your username" name="username" spellcheck="false" required />
</div> </div>
<div class="text-center"> <div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5"> <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Send Reset Code</span> <span data-i18n="login.send_reset_code" class="indicator-label">Send Reset Code</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "baselogin" .}} {{- template "baselogin" .}}
{{- define "title"}}Login{{- end}}
{{- define "content"}} {{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST"> <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10"> <div class="container mb-10">
@ -34,13 +32,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{template "errmsg" .Error}} {{template "errmsg" .Error}}
{{- if not .FormDisabled}} {{- if not .FormDisabled}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" name="username" placeholder="Username" spellcheck="false" required /> <input data-i18n="[placeholder]login.username" class="form-control form-control-lg form-control-solid" type="text" name="username" placeholder="Username" spellcheck="false" required />
</div> </div>
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="Password" autocomplete="current-password" spellcheck="false" required /> <input data-i18n="[placeholder]login.password" class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="Password" autocomplete="current-password" spellcheck="false" required />
<div class="d-flex justify-content-end mt-2"> <div class="d-flex justify-content-end mt-2">
{{- if .ForgotPwdURL}} {{- if .ForgotPwdURL}}
<a href="{{.ForgotPwdURL}}" class="link-primary fs-6 fw-bold">Forgot Password ?</a> <a data-i18n="login.forgot_password" href="{{.ForgotPwdURL}}" class="link-primary fs-6 fw-bold">Forgot Password ?</a>
{{- end}} {{- end}}
</div> </div>
</div> </div>
@ -49,18 +47,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- if not .FormDisabled}} {{- if not .FormDisabled}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5"> <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Sign in</span> <span data-i18n="login.signin" class="indicator-label">Sign in</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
{{- end}} {{- end}}
{{- if .OpenIDLoginURL}} {{- if .OpenIDLoginURL}}
{{- if not .FormDisabled}} {{- if not .FormDisabled}}
<div class="text-center text-muted text-uppercase fw-bold mb-5">or</div> <div data-i18n="general.or" class="text-center text-muted text-uppercase fw-bold mb-5">or</div>
{{- end}} {{- end}}
<a href="{{.OpenIDLoginURL}}" class="btn btn-flex flex-center btn-light btn-lg w-100 mb-5"> <a href="{{.OpenIDLoginURL}}" class="btn btn-flex flex-center btn-light btn-lg w-100 mb-5">
<img alt="Logo" src="{{.StaticURL}}/img/openid-logo.png" class="h-20px me-3" />Sign in with OpenID</a> <img data-i18n="login.signin_openid" alt="Logo" src="{{.StaticURL}}/img/openid-logo.png" class="h-20px me-3" />Sign in with OpenID</a>
{{- end}} {{- end}}
</div> </div>
</form> </form>

View file

@ -15,7 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20"> <div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
{{- if not .LoggedUser.Username}} {{- if not .LoggedUser.Username}}
@ -41,7 +40,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- end}} {{- end}}
<div class="card shadow-sm w-lg-600px"> <div class="card shadow-sm w-lg-600px">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">{{.Title}}</h3> <h3 data-i18n="{{.Title}}" class="card-title text-primary"></h3>
</div> </div>
<div class="card-body"> <div class="card-body">
{{- if .Error}} {{- if .Error}}
@ -54,7 +53,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex flex-stack flex-grow-1 "> <div class="d-flex flex-stack flex-grow-1 ">
<div class=" fw-semibold"> <div class=" fw-semibold">
<div class="fs-5 text-gray-800"> <div class="fs-5 text-gray-800">
{{.Error}} <span data-i18n="{{.Error}}"></span>
</div> </div>
</div> </div>
</div> </div>
@ -70,7 +69,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex flex-stack flex-grow-1 "> <div class="d-flex flex-stack flex-grow-1 ">
<div class=" fw-semibold"> <div class=" fw-semibold">
<div class="fs-5 text-gray-800"> <div class="fs-5 text-gray-800">
{{.Success}} <span data-i18n="{{.Success}}"></span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,12 +15,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">Two-factor authentication using Authenticator apps</h3> <h3 data-i18n="2fa.title" class="card-title text-primary">Two-factor authentication using Authenticator apps</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5"> <div class="notice d-flex bg-light-primary rounded border-primary border border-dashed p-6 mb-5">
@ -32,23 +30,26 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="mb-3 mb-md-0 fw-semibold"> <div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold"> <h4 class="text-gray-900 fw-bold">
{{- if .TOTPConfig.Enabled}} {{- if .TOTPConfig.Enabled}}
Two-factor authentication is enabled {{- if gt (len .TOTPConfigs) 1 }}. Configuration: "{{$.TOTPConfig.ConfigName}}" {{- end}} <span data-i18n="2fa.msg_enabled">Two-factor authentication is enabled</span> {{- if gt (len .TOTPConfigs) 1 }} ({{$.TOTPConfig.ConfigName}}) {{- end}}
{{- else}} {{- else}}
Secure Your Account <span data-i18n="2fa.msg_disabled">Secure Your Account</span>
{{- end}} {{- end}}
</h4> </h4>
<div class="fs-6 text-gray-800 pe-7"> <div class="fs-6 text-gray-800 pe-7">
<span data-i18n="2fa.msg_info">
Two-factor authentication adds an extra layer of security to your account. To log in you'll need Two-factor authentication adds an extra layer of security to your account. To log in you'll need
to provide an additional authentication code. to provide an additional authentication code.
</span>
</div> </div>
</div> </div>
{{- if .TOTPConfig.Enabled}} {{- if .TOTPConfig.Enabled}}
<button type="button" id="disable_btn" class="btn btn-danger ms-4 px-6 align-self-center text-nowrap"> <button type="button" id="disable_btn" class="btn btn-danger ms-4 px-6 align-self-center text-nowrap">
<span class="indicator-label"> <span data-i18n="general.disable" class="indicator-label">
Disable Disable
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
{{- end}} {{- end}}
@ -58,10 +59,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- template "errmsg" ""}} {{- template "errmsg" ""}}
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Configuration</label> <label data-i18n="general.configuration" class="col-md-3 col-form-label">Configuration</label>
<div class="col-md-9"> <div class="col-md-9">
<select id="id_config" name="config_name" class="form-select" data-control="select2" data-hide-search="true"> <select id="id_config" name="config_name" class="form-select" data-control="i18n-select2" data-hide-search="true">
<option value="">None</option> <option data-i18n="general.none" value="">None</option>
{{range .TOTPConfigs}} {{range .TOTPConfigs}}
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option> <option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
{{end}} {{end}}
@ -70,11 +71,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label required"> <label data-i18n="2fa.require_for" class="col-md-3 col-form-label required">
Require 2FA for Require 2FA for
</label> </label>
<div class="col-md-9"> <div class="col-md-9">
<select id="id_protocols" name="multi_factor_protocols" class="form-select" data-control="select2" data-hide-search="true" data-close-on-select="false" multiple="multiple"> <select id="id_protocols" name="multi_factor_protocols" class="form-select" data-control="i18n-select2" data-hide-search="true" data-close-on-select="false" multiple="multiple">
<option></option> <option></option>
{{range $protocol := .Protocols}} {{range $protocol := .Protocols}}
<option value="{{$protocol}}" {{range $p :=$.TOTPConfig.Protocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}} <option value="{{$protocol}}" {{range $p :=$.TOTPConfig.Protocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
@ -87,18 +88,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex justify-content-end mt-15"> <div class="d-flex justify-content-end mt-15">
{{- if .TOTPConfig.Enabled }} {{- if .TOTPConfig.Enabled }}
<button type="button" id="generate_secret_btn" class="btn btn-light-primary px-10 me-10 d-none"> <button type="button" id="generate_secret_btn" class="btn btn-light-primary px-10 me-10 d-none">
<span class="indicator-label"> <span data-i18n="2fa.generate" class="indicator-label">
Generate new secret Generate new secret
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
{{- end}} {{- end}}
<button type="button" id="save_btn" class="btn btn-primary px-10 d-none"> <button type="button" id="save_btn" class="btn btn-primary px-10 d-none">
<span id="save_label" class="indicator-label"></span> <span id="save_label" class="indicator-label"></span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
</div> </div>
@ -114,27 +117,29 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</i> </i>
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap"> <div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold"> <div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold"> <h4 data-i18n="2fa.recovery_codes" class="text-gray-900 fw-bold">
Recovery codes Recovery codes
</h4> </h4>
<div class="fs-6 text-gray-800"> <div class="fs-6 text-gray-800">
<p>Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.</p> <p data-i18n="2fa.recovery_codes_msg1">Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor configuration.</p>
<p>To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p> <p data-i18n="2fa.recovery_codes_msg2">To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.</p>
<p>If you generate new recovery codes, you automatically invalidate old ones.</p> <p data-i18n="2fa.recovery_codes_msg3">If you generate new recovery codes, you automatically invalidate old ones.</p>
</div> </div>
<div class="d-flex justify-content-center mt-10"> <div class="d-flex justify-content-center mt-10">
<button type="button" id="generate_recovery_code_btn" class="btn btn-primary px-10 me-10"> <button type="button" id="generate_recovery_code_btn" class="btn btn-primary px-10 me-10">
<span class="indicator-label"> <span data-i18n="general.generate" class="indicator-label">
Generate Generate
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
<button type="button" id="view_recovery_code_btn" class="btn btn-primary px-10"> <button type="button" id="view_recovery_code_btn" class="btn btn-primary px-10">
<span id="save_label" class="indicator-label">View</span> <span data-i18n="general.view" id="save_label" class="indicator-label">View</span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
</div> </div>
@ -162,17 +167,16 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 id="idRecoveryCodesTitle" class="modal-title"></h3> <h3 id="idRecoveryCodesTitle" class="modal-title"></h3>
<div class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="idRecoveryCodesList" class="d-flex flex-column"> <div id="idRecoveryCodesList" class="d-flex flex-column">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button> <button data-i18n="general.ok" class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
</div> </div>
</div> </div>
</div> </div>
@ -182,23 +186,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">Learn about two-factor authentication</h3> <h3 data-i18n="2fa.info_title" class="modal-title">Learn about two-factor authentication</h3>
<div class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
<div class="modal-body fw-semibold fs-6"> <div class="modal-body fw-semibold fs-6">
<p>SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive <p data-i18n="2fa.info1">SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive authentication.</p>
authentication.</p> <p data-i18n="2fa.info2">HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API
<p>HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API
you have to add the passcode using an HTTP header.</p> you have to add the passcode using an HTTP header.</p>
<p>FTP has no standard way to support two factor authentication, if you enable the FTP support, you have to add the <p data-i18n="2fa.info3">FTP has no standard way to support two factor authentication, if you enable the FTP support, you have to add the
TOTP passcode after the password. For example if your password is "password" and your one time passcode is TOTP passcode after the password. For example if your password is "password" and your one time passcode is
"123456" you have to use "password123456" as password.</p> "123456" you have to use "password123456" as password.</p>
<p>WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.</p> <p data-i18n="2fa.info4">WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button> <button data-i18n="general.ok" class="btn btn-primary" type="button" data-bs-dismiss="modal">OK</button>
</div> </div>
</div> </div>
</div> </div>
@ -208,14 +211,16 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-dialog modal-dialog-centered modal-lg"> <div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">Set up two-factor authentication</h3> <h3 data-i18n="2fa.setup_title" class="modal-title">Set up two-factor authentication</h3>
<div class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary ms-2" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
<div class="modal-body scroll-y pt-10 pb-15 px-lg-17"> <div class="modal-body scroll-y pt-10 pb-15 px-lg-17">
<div class="text-gray-700 fw-semibold fs-6 mb-10"> <div class="text-gray-700 fw-semibold fs-6 mb-10">
<span data-i18n="2fa.setup_msg">
Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below. Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.
</span>
</div> </div>
<div id="id_qr_code_container" class="pt-5 text-center"> <div id="id_qr_code_container" class="pt-5 text-center">
</div> </div>
@ -227,15 +232,16 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</i> </i>
<div class="d-flex flex-stack flex-grow-1"> <div class="d-flex flex-stack flex-grow-1">
<div class="fw-semibold"> <div class="fw-semibold">
<div class="fs-6 text-gray-800">If you have trouble using the QR code, select manual entry on your app, and enter the code: <div class="fs-6 text-gray-800">
<span data-i18n="2fa.setup_help">If you have trouble using the QR code, select manual entry on your app, and enter the code:</span>
<span id="id_secret" class="fw-bold text-gray-900 pt-2"></span> <span id="id_secret" class="fw-bold text-gray-900 pt-2"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="errorModalMsg" class="d-none alert alert-dismissible bg-light-warning d-flex align-items-center p-5 mb-10"> <div id="errorModalMsg" class="d-none rounded border-warning border border-dashed bg-light-warning d-flex align-items-center p-5 mb-10">
<i class="ki-duotone ki-information-5 fs-3x text-warning me-5"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i> <i class="ki-duotone ki-information fs-3x text-warning me-5"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i>
<div class="text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10"> <div class="text-gray-700 fw-bold fs-5 d-flex flex-column pe-0 pe-sm-10">
<span id="errorModalTxt"></span> <span id="errorModalTxt"></span>
</div> </div>
@ -245,17 +251,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="fv-row"> <div class="fv-row">
<input type="text" id="id_passcode" name="passcode" class="form-control form-control-lg form-control-solid" placeholder="Enter authentication code" spellcheck="false" /> <input data-i18n="[placeholder]login.auth_code" type="text" id="id_passcode" name="passcode" class="form-control form-control-lg form-control-solid" placeholder="Enter authentication code" spellcheck="false" />
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button data-i18n="general.cancel" type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary ms-6" id="passcode_btn"> <button type="button" class="btn btn-primary ms-6" id="passcode_btn">
<span class="indicator-label"> <span data-i18n="general.submit" class="indicator-label">
Submit Submit
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
</div> </div>
@ -277,15 +284,15 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} else { } else {
//{{- if .TOTPConfig.Enabled }} //{{- if .TOTPConfig.Enabled }}
if (selectedConfig == "{{.TOTPConfig.ConfigName}}"){ if (selectedConfig == "{{.TOTPConfig.ConfigName}}"){
$('#save_label').text("Save"); $('#save_label').text($.t('general.submit'));
} else { } else {
$('#save_label').text("Enable"); $('#save_label').text($.t('general.enable'));
} }
$('#save_btn').removeClass("d-none"); $('#save_btn').removeClass("d-none");
$('#generate_secret_btn').removeClass("d-none"); $('#generate_secret_btn').removeClass("d-none");
//{{- else}} //{{- else}}
$('#save_btn').removeClass("d-none"); $('#save_btn').removeClass("d-none");
$('#save_label').text("Enable"); $('#save_label').text($.t('general.enable'));
$('#generate_secret_btn').addClass("d-none"); $('#generate_secret_btn').addClass("d-none");
//{{- end}} //{{- end}}
} }
@ -307,7 +314,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then(function (response){ }).then(function (response){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
$('#idRecoveryCodesTitle').text("New recovery codes"); $('#idRecoveryCodesTitle').text($.t('2fa.new_recovery_codes'));
let recList = $('#idRecoveryCodesList'); let recList = $('#idRecoveryCodesList');
recList.empty(); recList.empty();
$.each(response.data, function(key, item) { $.each(response.data, function(key, item) {
@ -318,19 +325,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function (error){ }).catch(function (error){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
let errorMessage = "Failed to generate new recovery codes";
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
ModalAlert.fire({ ModalAlert.fire({
text: errorMessage, text: $.t('2fa.recovery_codes_gen_err'),
icon: "warning", icon: "warning",
confirmButtonText: "OK", confirmButtonText: $.t('general.ok'),
customClass: { customClass: {
confirmButton: "btn btn-danger" confirmButton: "btn btn-danger"
} }
@ -354,7 +352,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then(function (response){ }).then(function (response){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
$('#idRecoveryCodesTitle').text("Recovery codes"); $('#idRecoveryCodesTitle').text($.t('2fa.recovery_codes'));
let recList = $('#idRecoveryCodesList'); let recList = $('#idRecoveryCodesList');
recList.empty(); recList.empty();
$.each(response.data, function(key, item) { $.each(response.data, function(key, item) {
@ -369,19 +367,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function (error){ }).catch(function (error){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
let errorMessage = "Failed to get your recovery codes";
if (error && error.response) {
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
ModalAlert.fire({ ModalAlert.fire({
text: errorMessage, text: $.t('2fa.recovery_codes_get_err'),
icon: "warning", icon: "warning",
confirmButtonText: "OK", confirmButtonText: $.t('general.ok'),
customClass: { customClass: {
confirmButton: "btn btn-danger" confirmButton: "btn btn-danger"
} }
@ -391,10 +380,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
function disableConfig() { function disableConfig() {
ModalAlert.fire({ ModalAlert.fire({
text: `Do you want to disable two-factor authentication?`, text: $.t('2fa.disable_question'),
icon: "warning", icon: "warning",
confirmButtonText: "Confirm", confirmButtonText: $.t('general.confirm'),
cancelButtonText: 'Cancel', cancelButtonText: $.t('general.cancel'),
customClass: { customClass: {
confirmButton: "btn btn-danger", confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary' cancelButton: 'btn btn-secondary'
@ -410,12 +399,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
el = document.querySelector('#passcode_btn'); el = document.querySelector('#passcode_btn');
let errDivEl = $('#errorModalMsg'); let errDivEl = $('#errorModalMsg');
let errTxtEl = $('#errorModalTxt'); let errTxtEl = $('#errorModalTxt');
let errorMessage = "Failed to validate the provided authentication code"; let errorMessage = '2fa.auth_code_invalid';
let passcode = $('#id_passcode').val(); let passcode = $('#id_passcode').val();
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
if (passcode == "") { if (passcode == "") {
errTxtEl.text("The authentication code is required"); setI18NData(errTxtEl, '2fa.auth_code_required');
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -438,7 +427,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
if (!response.data.message) { if (!response.data.message) {
errTxtEl.text(errorMessage); setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -447,25 +436,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).catch(function (error){ }).catch(function (error){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
if (error && error.response) { setI18NData(errTxtEl, errorMessage);
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }
function confirmGenerateSecret() { function confirmGenerateSecret() {
ModalAlert.fire({ ModalAlert.fire({
text: `Do you want to generate a new secret and invalidate the previous one? Any registered Authenticator App will stop working`, text: $.t('2fa.generate_question'),
icon: "warning", icon: "warning",
confirmButtonText: "Confirm", confirmButtonText: $.t('general.confirm'),
cancelButtonText: 'Cancel', cancelButtonText: $.t('general.cancel'),
customClass: { customClass: {
confirmButton: "btn btn-danger", confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary' cancelButton: 'btn btn-secondary'
@ -488,12 +469,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let errDivEl = $('#errorMsg'); let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt'); let errTxtEl = $('#errorTxt');
if ($('#id_protocols').find('option:selected').length == 0){ if ($('#id_protocols').find('option:selected').length == 0){
errTxtEl.text("Please select at least a protocol"); setI18NData(errTxtEl, '2fa.no_protocol');
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
let errorMessage = "Failed to generate authentication secret"; let errorMessage = '2fa.auth_secret_gen_err';
$('#id_secret').text(""); $('#id_secret').text("");
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
@ -514,7 +495,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
if (!response.data.secret) { if (!response.data.secret) {
errTxtEl.text(errorMessage); setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -526,21 +507,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let qrCodeImg = document.createElement("img"); let qrCodeImg = document.createElement("img");
qrCodeImg.classList.add("mw-150px"); qrCodeImg.classList.add("mw-150px");
qrCodeImg.src = "{{.MFAURL}}/qrcode?url="+encodeURIComponent(response.data.url); qrCodeImg.src = "{{.MFAURL}}/qrcode?url="+encodeURIComponent(response.data.url);
qrCodeImg.alt = "QR code"; qrCodeImg.alt = $.t('general.qr_code');
qrCodeContainer.appendChild(qrCodeImg); qrCodeContainer.appendChild(qrCodeImg);
qrModal.show(); qrModal.show();
}).catch(function (error){ }).catch(function (error){
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
if (error && error.response) { setI18NData(errTxtEl, errorMessage);
if (error.response.data.message) {
errorMessage = error.response.data.message;
}
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
}
}
errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }
@ -554,13 +527,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
let errDivEl = $('#errorMsg'); let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt'); let errTxtEl = $('#errorTxt');
let errorMessage = "Failed to save configuration"; let errorMessage = '2fa.save_err';
let protocolsArray = []; let protocolsArray = [];
$('#id_protocols').find('option:selected').each(function(){ $('#id_protocols').find('option:selected').each(function(){
protocolsArray.push($(this).val()); protocolsArray.push($(this).val());
}); });
if (protocolsArray.length == 0){ if (protocolsArray.length == 0){
errTxtEl.text("Please select at least a protocol"); setI18NData(errTxtEl, '2fa.no_protocol');
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
@ -595,14 +568,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
if (!response.data.message) { if (!response.data.message) {
errTxtEl.text(errorMessage); setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
ModalAlert.fire({ ModalAlert.fire({
text: `Configuration saved`, text: $.t('general.config_saved'),
icon: "success", icon: "success",
confirmButtonText: "OK", confirmButtonText: $.t('general.ok'),
customClass: { customClass: {
confirmButton: 'btn btn-primary' confirmButton: 'btn btn-primary'
} }
@ -615,14 +588,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
el.removeAttribute('data-kt-indicator'); el.removeAttribute('data-kt-indicator');
el.disabled = false; el.disabled = false;
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 400:
} errorMessage = "2fa.save_err_proto";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error;
} }
} }
errTxtEl.text(errorMessage); setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
@ -638,7 +610,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
generateSecret(saveBtn, selectedConfig); generateSecret(saveBtn, selectedConfig);
} }
KTUtil.onDOMContentLoaded(function () { $(document).on("i18nshow", function(){
onConfigChanged(); onConfigChanged();
var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg'); var dismissErrorModalBtn = $('#id_dismiss_error_modal_msg');

View file

@ -15,18 +15,16 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">My profile - {{.LoggedUser.Username}}</h3> <h3 data-i18n="general.my_profile" class="card-title text-primary">My profile</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
{{- template "errmsg" .Error}} {{- template "errmsg" .Error}}
<form id="page_form" action="{{.CurrentURL}}" method="POST"> <form id="page_form" action="{{.CurrentURL}}" method="POST">
<div class="form-group row"> <div class="form-group row">
<label class="col-md-3 col-form-label">Email</label> <label data-i18n="general.email" class="col-md-3 col-form-label">Email</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control" id="idEmail" name="email" placeholder="" spellcheck="false" <input type="text" class="form-control" id="idEmail" name="email" placeholder="" spellcheck="false"
value="{{.Email}}" maxlength="255" autocomplete="nope" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}> value="{{.Email}}" maxlength="255" autocomplete="nope" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}>
@ -34,7 +32,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Description</label> <label data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control" id="idDescription" name="description" placeholder="" <input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Description}}" maxlength="255" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}> value="{{.Description}}" maxlength="255" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}>
@ -42,11 +40,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group row align-items-center mt-10"> <div class="form-group row align-items-center mt-10">
<label class="col-md-3 col-form-label">API key authentication</label> <label data-i18n="general.api_key_auth" class="col-md-3 col-form-label">API key authentication</label>
<div class="col-md-9"> <div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid"> <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 not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}} {{if .AllowAPIKeyAuth}}checked="checked"{{end}}/> <input class="form-check-input" type="checkbox" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}} {{if .AllowAPIKeyAuth}}checked="checked"{{end}}/>
<label class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth"> <label data-i18n="general.api_key_auth_help" class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth">
Allow to impersonate yourself, in REST API, using an API key Allow to impersonate yourself, in REST API, using an API key
</label> </label>
</div> </div>
@ -56,7 +54,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- if .LoggedUser.CanManagePublicKeys}} {{- if .LoggedUser.CanManagePublicKeys}}
<div class="card card-rounded mt-10"> <div class="card card-rounded mt-10">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title">Public keys</h3> <h3 data-i18n="general.pub_keys" class="card-title">Public keys</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="public_keys"> <div id="public_keys">
@ -66,11 +64,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div data-repeater-item> <div data-repeater-item>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-9"> <div class="col-md-9">
<textarea class="form-control mt-3 mt-md-8" name="public_key" rows="4" <textarea data-i18n="[placeholder]general.pub_key_placeholder" class="form-control mt-3 mt-md-8" name="public_key" rows="4"
placeholder="Paste your public key here">{{$val}}</textarea> placeholder="Paste your public key here">{{$val}}</textarea>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a href="#" data-repeater-delete <a data-i18n="general.delete" href="#" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8"> class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5"> <i class="ki-duotone ki-trash fs-5">
<span class="path1"></span> <span class="path1"></span>
@ -88,11 +86,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div data-repeater-item> <div data-repeater-item>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-9"> <div class="col-md-9">
<textarea class="form-control mt-3 mt-md-8" name="public_key" rows="4" <textarea data-i18n="[placeholder]general.pub_key_placeholder" class="form-control mt-3 mt-md-8" name="public_key" rows="4"
placeholder="Paste your public key here"></textarea> placeholder="Paste your public key here"></textarea>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a href="#" data-repeater-delete <a data-i18n="general.delete" href="#" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8"> class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5"> <i class="ki-duotone ki-trash fs-5">
<span class="path1"></span> <span class="path1"></span>
@ -111,7 +109,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group mt-5"> <div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary"> <a data-i18n="general.add" href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i> <i class="ki-duotone ki-plus fs-3"></i>
Add Add
</a> </a>
@ -123,11 +121,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex justify-content-end mt-12"> <div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10"> <button type="submit" id="form_submit" class="btn btn-primary px-10">
<span class="indicator-label"> <span data-i18n="general.submit" class="indicator-label">
Submit Submit
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
</div> </div>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "baselogin" .}} {{- template "baselogin" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "content"}} {{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST"> <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10"> <div class="container mb-10">
@ -34,28 +32,31 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
<div class="text-center mb-10"> <div class="text-center mb-10">
<h2 class="text-gray-900 mb-3"> <h2 data-i18n="login.reset_password" class="text-gray-900 mb-3">
Reset Password Reset Password
</h2> </h2>
<div class="text-gray-600 fw-semibold fs-4"> <div class="text-gray-600 fw-semibold fs-4">
<span data-i18n="login.reset_pwd_msg">
Check your email for the confirmation code Check your email for the confirmation code
</span>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" placeholder="Confirmation code" name="code" spellcheck="false" required /> <input data-i18n="[placeholder]login.confirm_code" class="form-control form-control-lg form-control-solid" type="text" placeholder="Confirmation code" name="code" spellcheck="false" required />
</div> </div>
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="New Password" autocomplete="new-password" spellcheck="false" required /> <input data-i18n="[placeholder]general.new_password" class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="New Password" autocomplete="new-password" spellcheck="false" required />
</div> </div>
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" spellcheck="false" required /> <input data-i18n="[placeholder]general.confirm_password" class="form-control form-control-lg form-control-solid" type="password" name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" spellcheck="false" required />
</div> </div>
<div class="text-center"> <div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5"> <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Update Password & Login</span> <span data-i18n="login.reset_submit" class="indicator-label">Update Password & Login</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>

View file

@ -15,18 +15,16 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">{{if .IsAdd}}Add a new share{{else}}Edit share{{end}}</h3> <h3 {{if .IsAdd}}data-i18n="title.add_share"{{else}}data-i18n="title.update_share"{{end}} class="card-title text-primary"></h3>
</div> </div>
<div class="card-body"> <div class="card-body">
{{- template "errmsg" .Error}} {{- template "errmsg" .Error}}
<form id="share_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"> <form id="share_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row"> <div class="form-group row">
<label class="col-md-3 col-form-label">Name</label> <label data-i18n="general.name" class="col-md-3 col-form-label">Name</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control" placeholder="" name="name" value="{{.Share.Name}}" <input type="text" class="form-control" placeholder="" name="name" value="{{.Share.Name}}"
maxlength="255" autocomplete="nope" required {{if not .IsAdd}}readonly{{end}} /> maxlength="255" autocomplete="nope" required {{if not .IsAdd}}readonly{{end}} />
@ -34,22 +32,22 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Scope</label> <label data-i18n="share.scope" class="col-md-3 col-form-label">Scope</label>
<div class="col-md-9"> <div class="col-md-9">
<select name="scope" class="form-select" data-control="select2" data-hide-search="true"> <select name="scope" class="form-select" data-control="i18n-select2" data-hide-search="true">
<option value="1" {{if eq .Share.Scope 1 }}selected{{end}}>Read</option> <option data-i18n="share.scope_read" value="1" {{if eq .Share.Scope 1 }}selected{{end}}>Read</option>
<option value="2" {{if eq .Share.Scope 2 }}selected{{end}}>Write</option> <option data-i18n="share.scope_write" value="2" {{if eq .Share.Scope 2 }}selected{{end}}>Write</option>
<option value="3" {{if eq .Share.Scope 3 }}selected{{end}}>Read/Write</option> <option data-i18n="share.scope_read_write" value="3" {{if eq .Share.Scope 3 }}selected{{end}}>Read/Write</option>
</select> </select>
<div class="form-text"> <div data-i18n="share.scope_help" class="form-text">
For scope "Write" and "Read&Write" you have to define one path and it must be a directory For scope "Write" and "Read/Write" you have to define one path and it must be a directory
</div> </div>
</div> </div>
</div> </div>
<div class="card card-rounded mt-10"> <div class="card card-rounded mt-10">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title">Paths</h3> <h3 data-i18n="share.paths" class="card-title">Paths</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="paths"> <div id="paths">
@ -59,12 +57,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div data-repeater-item> <div data-repeater-item>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control mt-3 mt-md-8" <input data-i18n="[placeholder]share.path_help" type="text" class="form-control mt-3 mt-md-8"
placeholder="file or directory path, i.e. /dir or /dir/file.txt"
name="path" value="{{$val}}" /> name="path" value="{{$val}}" />
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a href="#" data-repeater-delete <a data-i18n="general.delete" href="#" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8"> class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5"> <i class="ki-duotone ki-trash fs-5">
<span class="path1"></span> <span class="path1"></span>
@ -82,12 +79,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div data-repeater-item> <div data-repeater-item>
<div class="form-group row"> <div class="form-group row">
<div class="col-md-9"> <div class="col-md-9">
<input type="text" class="form-control mt-3 mt-md-8" <input data-i18n="[placeholder]share.path_help" type="text" class="form-control mt-3 mt-md-8"
placeholder="file or directory path, i.e. /dir or /dir/file.txt"
name="path" value="" /> name="path" value="" />
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a href="#" data-repeater-delete <a data-i18n="general.delete" href="#" data-repeater-delete
class="btn btn-light-danger mt-3 mt-md-8"> class="btn btn-light-danger mt-3 mt-md-8">
<i class="ki-duotone ki-trash fs-5"> <i class="ki-duotone ki-trash fs-5">
<span class="path1"></span> <span class="path1"></span>
@ -106,7 +102,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group mt-5"> <div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary"> <a data-i18n="general.add" href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i> <i class="ki-duotone ki-plus fs-3"></i>
Add Add
</a> </a>
@ -116,20 +112,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Password</label> <label data-i18n="login.password" class="col-md-3 col-form-label">Password</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" class="form-control" name="password" autocomplete="new-password" <input type="password" class="form-control" name="password" autocomplete="new-password"
placeholder="" spellcheck="false" value="{{.Share.Password}}" /> placeholder="" spellcheck="false" value="{{.Share.Password}}" />
<div class="form-text"> <div data-i18n="share.password_help" class="form-text">
If set the share will be password-protected If set the share will be password-protected
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Expiration</label> <label data-i18n="share.expiration" class="col-md-3 col-form-label">Expiration</label>
<div class="col-md-9 d-flex"> <div class="col-md-9 d-flex">
<input id="id_expiration" class="form-control" placeholder="Pick an expiration date" /> <input data-i18n="[placeholder]share.expiration_help" id="id_expiration" class="form-control" placeholder="Pick an expiration date" />
<button class="btn btn-icon btn-light-danger ms-2 d-none" id="id_expiration_clear"> <button class="btn btn-icon btn-light-danger ms-2 d-none" id="id_expiration_clear">
<i class="ki-duotone ki-cross fs-1"> <i class="ki-duotone ki-cross fs-1">
<span class="path1"></span> <span class="path1"></span>
@ -140,28 +136,28 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Max tokens</label> <label data-i18n="share.max_tokens" class="col-md-3 col-form-label">Max tokens</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="number" min="0" class="form-control" name="max_tokens" value="{{.Share.MaxTokens}}" /> <input type="number" min="0" class="form-control" name="max_tokens" value="{{.Share.MaxTokens}}" />
<div class="form-text"> <div data-i18n="share.max_tokens_help" class="form-text">
Maximum number of times this share can be accessed. 0 means no limit Maximum number of times this share can be accessed. 0 means no limit
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Allowed IP/Mask</label> <label data-i18n="general.allowed_ip_mask" class="col-md-3 col-form-label">Allowed IP/Mask</label>
<div class="col-md-9"> <div class="col-md-9">
<textarea class="form-control" name="allowed_ip" rows="3" <textarea class="form-control" name="allowed_ip" rows="3"
placeholder="">{{.Share.GetAllowedFromAsString}}</textarea> placeholder="">{{.Share.GetAllowedFromAsString}}</textarea>
<div class="form-text"> <div data-i18n="general.allowed_ip_mask_help" class="form-text">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32" Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row mt-10"> <div class="form-group row mt-10">
<label class="col-md-3 col-form-label">Description</label> <label data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
<div class="col-md-9"> <div class="col-md-9">
<textarea class="form-control" name="description" rows="3" <textarea class="form-control" name="description" rows="3"
placeholder="">{{.Share.Description}}</textarea> placeholder="">{{.Share.Description}}</textarea>
@ -172,11 +168,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<input type="hidden" name="expiration_date" id="hidden_start_datetime" value=""> <input type="hidden" name="expiration_date" id="hidden_start_datetime" value="">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10"> <button type="submit" id="form_submit" class="btn btn-primary px-10">
<span class="indicator-label"> <span data-i18n="general.submit" class="indicator-label">
Submit Submit
</span> </span>
<span class="indicator-progress"> <span data-i18n="general.wait" class="indicator-progress">
Please wait... <span class="spinner-border spinner-border-sm align-middle ms-2"></span> Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
</div> </div>
@ -188,14 +185,21 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- define "extra_js"}} {{- define "extra_js"}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
KTUtil.onDOMContentLoaded(function () { $(document).on("i18nshow", function(){
initRepeater('#paths'); initRepeater('#paths');
initRepeaterItems(); initRepeaterItems();
const picker = $('#id_expiration').flatpickr({ const picker = $('#id_expiration').flatpickr({
enableTime: false, enableTime: false,
time_24hr: true, time_24hr: true,
dateFormat: "Y-m-d", formatDate: (date, format, locale) => {
return $.t('general.datetime', {
val: new Date(date),
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
}
});
},
defaultHour: 23, defaultHour: 23,
defaultMinute: 59, defaultMinute: 59,
onChange: function(selectedDates, dateStr, instance) { onChange: function(selectedDates, dateStr, instance) {

View file

@ -14,7 +14,7 @@ therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com). explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20"> <div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
<div class="mb-12"> <div class="mb-12">
@ -27,11 +27,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="card shadow-sm w-lg-600px"> <div class="card shadow-sm w-lg-600px">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">Your download is ready</h3> <h3 data-i18n="fs.download_ready" class="card-title text-primary">Your download is ready</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div> <div>
<a href="{{.DownloadLink}}" class="btn btn-primary btn-user-custom btn-block">Download</a> <a data-i18n="fs.download" href="{{.DownloadLink}}" class="btn btn-primary btn-user-custom btn-block">Download</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "baselogin" .}} {{- template "baselogin" .}}
{{- define "title"}}Share login{{- end}}
{{- define "content"}} {{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST"> <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10"> <div class="container mb-10">
@ -33,13 +31,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
{{template "errmsg" .Error}} {{template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="password" name="share_password" placeholder="Password" spellcheck="false" required /> <input data-i18n="[placeholder]login.password" class="form-control form-control-lg form-control-solid" type="password" name="share_password" placeholder="Password" spellcheck="false" required />
</div> </div>
<div class="text-center"> <div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5"> <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Sign in</span> <span data-i18n="login.signin" class="indicator-label">Sign in</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "extra_css"}} {{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/> <link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}} {{- end}}
@ -26,13 +24,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">View and manage shares</h3> <h3 data-i18n="share.view_manage" class="card-title text-primary">View and manage shares</h3>
</div> </div>
<div id="card_body" class="card-body"> <div id="card_body" class="card-body">
{{- template "errmsg" ""}} {{- template "errmsg" ""}}
<div id="loader" class="align-items-center text-center my-10"> <div id="loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span> <span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span class="text-gray-600">Loading...</span> <span data-i18n="general.loading" class="text-gray-600">Loading...</span>
</div> </div>
<div id="card_content" class="d-none"> <div id="card_content" class="d-none">
<div class="d-flex flex-stack mb-5"> <div class="d-flex flex-stack mb-5">
@ -41,12 +39,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<span class="path1"></span> <span class="path1"></span>
<span class="path2"></span> <span class="path2"></span>
</i> </i>
<input type="text" data-share-table-filter="search" <input data-i18n="[placeholder]general.search" type="text" data-share-table-filter="search"
class="form-control form-control-solid w-200px ps-15" placeholder="Search" /> class="form-control form-control-solid w-200px ps-15" placeholder="Search" />
</div> </div>
<div class="d-flex justify-content-end" data-share-table-toolbar="base"> <div class="d-flex justify-content-end" data-share-table-toolbar="base">
<a href="{{.ShareURL}}" class="btn btn-primary"> <a data-i18n="general.add" href="{{.ShareURL}}" class="btn btn-primary">
<i class="ki-duotone ki-plus fs-2"></i> <i class="ki-duotone ki-plus fs-2"></i>
Add Add
</a> </a>
@ -56,13 +54,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5"> <table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead> <thead>
<tr class="text-start text-muted fw-bold fs-6 gs-0"> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th>Name</th> <th data-i18n="general.name">Name</th>
<th>Scope</th> <th data-i18n="share.scope">Scope</th>
<th>Info</th> <th data-i18n="general.info">Info</th>
<th class="min-w-100px"></th> <th class="min-w-100px"></th>
</tr> </tr>
</thead> </thead>
<tbody class="text-gray-800 fw-semibold"></tbody> <tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table> </table>
</div> </div>
</div> </div>
@ -77,7 +75,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<h3 class="modal-title"> <h3 class="modal-title">
Share access links Share access links
</h3> </h3>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close"> <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
</div> </div>
</div> </div>
@ -114,10 +112,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
ModalAlert.fire({ ModalAlert.fire({
text: `Do you want to delete the selected share? This action is irreversible`, text: $.t('general.delete_confirm_generic'),
icon: "warning", icon: "warning",
confirmButtonText: "Delete", confirmButtonText: $.t('general.delete'),
cancelButtonText: 'Cancel', cancelButtonText: $.t('general.cancel'),
customClass: { customClass: {
confirmButton: "btn btn-danger", confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary' cancelButton: 'btn btn-secondary'
@ -140,16 +138,21 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
location.reload(); location.reload();
}).catch(function(error){ }).catch(function(error){
KTApp.hidePageLoading(); KTApp.hidePageLoading();
let errorMessage = `Unable to delete the selected share`; let errorMessage;
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
} errorMessage = "general.delete_error_403";
if (error.response.data.error) { break;
errorMessage += ": " + error.response.data.error; case 404:
errorMessage = "general.delete_error_404";
break;
} }
} }
errTxtEl.text(errorMessage); if (!errorMessage){
errorMessage = "general.delete_error_generic";
}
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }
@ -167,7 +170,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
$('#readShare').hide(); $('#readShare').hide();
} else { } else {
let shareURL = '{{.BasePublicSharesURL}}' + "/" + encodeURIComponent(shareID); let shareURL = '{{.BasePublicSharesURL}}' + "/" + encodeURIComponent(shareID);
if (shareScope == 'Read') { if (shareScope == '1') {
$('#expiredShare').hide(); $('#expiredShare').hide();
$('#writeShare').hide(); $('#writeShare').hide();
$('#readShare').show(); $('#readShare').show();
@ -190,7 +193,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
const tableData = []; const tableData = [];
{{- range .Shares}} {{- range .Shares}}
tableData.push(['{{.Name}}','{{.GetScopeAsString}}','{{.GetInfoString}}','{{.ShareID}}','{{- if .IsExpired}}1{{- else}}0{{- end}}']); tableData.push(['{{.Name}}','{{.Scope}}','{{- if .Password}}1{{- else}}0{{- end}}','{{.ShareID}}','{{- if .IsExpired}}1{{- else}}0{{- end}}', '{{.ExpiresAt}}', '{{.LastUseAt}}', '{{.UsedTokens}}', '{{.MaxTokens}}']);
{{- end}} {{- end}}
var sharesDatatable = function(){ var sharesDatatable = function(){
@ -200,6 +203,67 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
dt = $('#dataTable').DataTable({ dt = $('#dataTable').DataTable({
data: tableData, data: tableData,
columnDefs: [ columnDefs: [
{
target: 0,
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
target: 1,
render: function (data, type, row) {
if (type === 'display') {
switch (data){
case "2":
return $.t('share.scope_write');
case "3":
return $.t('share.scope_read_write');
default:
return $.t('share.scope_read');
}
}
return data;
}
},
{
target: 2,
searchable: false,
orderable: false,
render: function (data, type, row) {
if (type === 'display') {
let info = "";
if (row[5] > 0){
info+= $.t('share.expiration_date', {
val: new Date(parseInt(row[5], 10)),
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
}
});
}
if (row[6] > 0){
info+= $.t('share.last_use', {
val: new Date(parseInt(row[6], 10)),
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
}
});
}
if (row[8] > 0){
info+= $.t('share.usage', {used: row[7], total: row[8]})
} else {
info+= $.t('share.used_tokens', {used: row[7]})
}
if (data == "1"){
info+= $.t('share.password_protected')
}
return info;
}
return data;
}
},
{ {
targets: 3, targets: 3,
searchable: false, searchable: false,
@ -228,10 +292,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</button> </button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-semibold fs-7 w-150px py-4" data-kt-menu="true"> <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-semibold fs-7 w-150px py-4" data-kt-menu="true">
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a> <a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a>
</div> </div>
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a> <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a>
</div> </div>
</div> </div>
</div> </div>
@ -239,15 +303,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
return ""; return "";
} }
},
{
targets: [0, 2],
render: function (data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data
}
} }
], ],
deferRender: true, deferRender: true,
@ -260,7 +315,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
}, },
language: { language: {
emptyTable: "No share defined" info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('share.no_share')
}, },
order: [[1, 'asc']], order: [[1, 'asc']],
initComplete: function(settings, json) { initComplete: function(settings, json) {
@ -270,12 +331,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
api.columns.adjust().draw("page"); api.columns.adjust().draw("page");
KTMenu.createInstances(); KTMenu.createInstances();
handleRowActions(); handleRowActions();
$('#table_body').localize();
} }
}); });
dt.on('draw', function () { dt.on('draw', function () {
KTMenu.createInstances(); KTMenu.createInstances();
handleRowActions(); handleRowActions();
$('#table_body').localize();
}); });
} }
@ -327,7 +390,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
}(); }();
KTUtil.onDOMContentLoaded(function () { $(document).on("i18nshow", function(){
sharesDatatable.init(); sharesDatatable.init();
}); });
</script> </script>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "base" .}} {{- template "base" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "page_body"}} {{- define "page_body"}}
<div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20"> <div class="d-flex flex-center flex-column flex-column-fluid p-10 pb-lg-20">
<div class="mb-12"> <div class="mb-12">
@ -29,7 +27,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
<div class="card shadow-sm w-lg-600px"> <div class="card shadow-sm w-lg-600px">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h3 class="card-title text-primary">Upload one or more files to share "{{.Share.Name}}"</h3> <h3 data-i18n="title.upload_to_share" class="card-title text-primary">Upload one or more files to share</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
{{- template "errmsg" ""}} {{- template "errmsg" ""}}
@ -39,13 +37,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="dz-message needsclick align-items-center"> <div class="dz-message needsclick align-items-center">
<i class="ki-duotone ki-file-up fs-3x text-primary"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-file-up fs-3x text-primary"><span class="path1"></span><span class="path2"></span></i>
<div class="ms-4"> <div class="ms-4">
<h3 class="fs-5 fw-bold text-gray-900 mb-1">Drop files here or click to upload.</h3> <h3 data-i18n="fs.upload.message" class="fs-5 fw-bold text-gray-900 mb-1">Drop files here or click to upload.</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="d-flex justify-content-end mt-10"> <div class="d-flex justify-content-end mt-10">
<button type="button" id="upload_files_button" class="btn btn-primary">Submit</button> <button data-i18n="general.submit" type="button" id="upload_files_button" class="btn btn-primary">Submit</button>
</div> </div>
</form> </form>
</div> </div>
@ -69,9 +67,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
KTApp.hidePageLoading(); KTApp.hidePageLoading();
if (!has_errors) { if (!has_errors) {
ModalAlert.fire({ ModalAlert.fire({
text: `File/s uploaded successfully`, text: $.t('fs.upload.success'),
icon: "success", icon: "success",
confirmButtonText: "OK", confirmButtonText: $.t('general.ok'),
customClass: { customClass: {
confirmButton: 'btn btn-primary' confirmButton: 'btn btn-primary'
} }
@ -90,13 +88,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
try { try {
lastModified = f.lastModified; lastModified = f.lastModified;
} catch (e) { } catch (e) {
console.log("unable to get last modified time from file: " + e.message); console.error("unable to get last modified time from file: " + e.message);
lastModified = ""; lastModified = "";
} }
let uploadTxt = f.name; let uploadTxt = f.name;
if (files.length > 1){ if (files.length > 1){
uploadTxt = `Upload ${index+1}/${files.length}: ${uploadTxt}`; uploadTxt = $.t('fs.uploading', {
idx: index + 1,
total: files.length,
name: uploadTxt
});
} }
$('#loading_message').text(uploadTxt); $('#loading_message').text(uploadTxt);
@ -123,18 +125,23 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
success++; success++;
uploadFile(); uploadFile();
}).catch(function (error) { }).catch(function (error) {
let errorMessage = "Error uploading files"; let errorMessage;
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { switch (error.response.status) {
errorMessage = error.response.data.message; case 403:
errorMessage = "fs.upload.err_403";
break;
case 429:
errorMessage = "fs.upload.err_429";
break;
} }
if (error.response.data.error) {
errorMessage += ": " + error.response.data.error;
} }
if (!errorMessage){
errorMessage = "fs.upload.err_generic";
} }
index++; index++;
has_errors = true; has_errors = true;
$('#errorTxt').text(errorMessage); setI18NData($('#errorTxt'), errorMessage);
$('#errorMsg').removeClass("d-none"); $('#errorMsg').removeClass("d-none");
uploadFile(); uploadFile();
}); });

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "baselogin" .}} {{- template "baselogin" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "content"}} {{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST"> <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10"> <div class="container mb-10">
@ -33,13 +31,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
{{template "errmsg" .Error}} {{template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" name="recovery_code" placeholder="Recovery code" spellcheck="false" required /> <input data-i18n="[placeholder]login.recovery_code" class="form-control form-control-lg form-control-solid" type="text" name="recovery_code" placeholder="Recovery code" spellcheck="false" required />
</div> </div>
<div class="text-center"> <div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5"> <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Verify</span> <span data-i18n="general.verify" class="indicator-label">Verify</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
@ -53,7 +52,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap"> <div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold"> <div class="mb-3 mb-md-0 fw-semibold">
<div class="fs-6 text-gray-800"> <div class="fs-6 text-gray-800">
<span data-i18n="login.recovery_code_msg">
You can enter one of your recovery codes in case you lost access to your mobile device. You can enter one of your recovery codes in case you lost access to your mobile device.
</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{- template "baselogin" .}} {{- template "baselogin" .}}
{{- define "title"}}{{.Title}}{{- end}}
{{- define "content"}} {{- define "content"}}
<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST"> <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
<div class="container mb-10"> <div class="container mb-10">
@ -33,13 +31,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
{{template "errmsg" .Error}} {{template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<input class="form-control form-control-lg form-control-solid" type="text" placeholder="Authentication code" name="passcode" spellcheck="false" required /> <input data-i18n="[placeholder]login.auth_code" class="form-control form-control-lg form-control-solid" type="text" placeholder="Authentication code" name="passcode" spellcheck="false" required />
</div> </div>
<div class="text-center"> <div class="text-center">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5"> <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
<span class="indicator-label">Verify</span> <span data-i18n="general.verify" class="indicator-label">Verify</span>
<span class="indicator-progress">Please wait... <span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span> <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span> </span>
</button> </button>
@ -54,11 +53,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap"> <div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold"> <div class="mb-3 mb-md-0 fw-semibold">
<div class="fs-6 text-gray-700"> <div class="fs-6 text-gray-700">
<span data-i18n="login.two_factor_help">
Open the two-factor authentication app on your device to view your authentication code and verify your identity. Open the two-factor authentication app on your device to view your authentication code and verify your identity.
</span>
</div> </div>
<div class="fs-6 text-gray-800 mt-5"> <div class="fs-6 text-gray-800 mt-5">
<p class="fw-bold">Having problems?</p> <p data-i18n="general.problems" class="fw-bold">Having problems?</p>
<p><a href="{{.RecoveryURL}}">Enter a two-factor recovery code</a></p> <p><a data-i18n="login.two_factor_msg" href="{{.RecoveryURL}}">Enter a two-factor recovery code</a></p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -18,7 +18,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{.Title}}</title> <title></title>
{{range .Branding.ExtraCSS}} {{range .Branding.ExtraCSS}}
<link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css"> <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">