WebClient: allow to pass args for localized errors from the backend

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-12-12 18:04:14 +01:00
parent 691133d7c8
commit 61fe7c39a7
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
26 changed files with 433 additions and 207 deletions

12
go.mod
View file

@ -4,7 +4,7 @@ go 1.21
require ( require (
cloud.google.com/go/storage v1.35.1 cloud.google.com/go/storage v1.35.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
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
@ -140,7 +140,7 @@ require (
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
@ -169,10 +169,10 @@ require (
golang.org/x/tools v0.16.0 // indirect golang.org/x/tools v0.16.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 // indirect
google.golang.org/grpc v1.59.0 // indirect google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

24
go.sum
View file

@ -12,8 +12,8 @@ cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXS
cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
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.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
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.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
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.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
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/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
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.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
@ -302,8 +302,8 @@ github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@ -524,19 +524,19 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 h1:W12Pwm4urIbRdGhMEg2NM9O3TWKjNcxQhs46V0ypf/k= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 h1:ZcOkrmX74HbKFYnpPY8Qsw93fC29TbJXspYKaBkSXDQ= google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA=
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 h1:kzJAXnzZoFbe5bhZd4zjUuHos/I31yH4thfMb/13oVY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -16,18 +16,47 @@ package httpd
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"net/http" "net/http"
"time" "time"
"github.com/drakkan/sftpgo/v2/internal/util"
) )
const ( const (
flashCookieName = "message" flashCookieName = "message"
) )
func setFlashMessage(w http.ResponseWriter, r *http.Request, value string) { func newFlashMessage(errorStrig, i18nMessage string) flashMessage {
return flashMessage{
ErrorString: errorStrig,
I18nMessage: i18nMessage,
}
}
type flashMessage struct {
ErrorString string `json:"error"`
I18nMessage string `json:"message"`
}
func (m *flashMessage) getI18nError() *util.I18nError {
if m.ErrorString == "" && m.I18nMessage == "" {
return nil
}
return util.NewI18nError(
util.NewGenericError(m.ErrorString),
m.I18nMessage,
)
}
func setFlashMessage(w http.ResponseWriter, r *http.Request, message flashMessage) {
value, err := json.Marshal(message)
if err != nil {
return
}
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: flashCookieName, Name: flashCookieName,
Value: base64.URLEncoding.EncodeToString([]byte(value)), Value: base64.URLEncoding.EncodeToString(value),
Path: "/", Path: "/",
Expires: time.Now().Add(60 * time.Second), Expires: time.Now().Add(60 * time.Second),
MaxAge: 60, MaxAge: 60,
@ -38,10 +67,11 @@ func setFlashMessage(w http.ResponseWriter, r *http.Request, value string) {
w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`) w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
} }
func getFlashMessage(w http.ResponseWriter, r *http.Request) string { func getFlashMessage(w http.ResponseWriter, r *http.Request) flashMessage {
var msg flashMessage
cookie, err := r.Cookie(flashCookieName) cookie, err := r.Cookie(flashCookieName)
if err != nil { if err != nil {
return "" return msg
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: flashCookieName, Name: flashCookieName,
@ -53,9 +83,13 @@ func getFlashMessage(w http.ResponseWriter, r *http.Request) string {
Secure: isTLS(r), Secure: isTLS(r),
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}) })
message, err := base64.URLEncoding.DecodeString(cookie.Value) value, err := base64.URLEncoding.DecodeString(cookie.Value)
if err != nil { if err != nil {
return "" return msg
} }
return string(message) err = json.Unmarshal(value, &msg)
if err != nil {
return flashMessage{}
}
return msg
} }

View file

@ -30,10 +30,10 @@ func TestFlashMessages(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/url", nil) req, err := http.NewRequest(http.MethodGet, "/url", nil)
require.NoError(t, err) require.NoError(t, err)
message := "test message" message := "test message"
setFlashMessage(rr, req, message) setFlashMessage(rr, req, flashMessage{ErrorString: message})
req.Header.Set("Cookie", fmt.Sprintf("%v=%v", flashCookieName, base64.URLEncoding.EncodeToString([]byte(message)))) req.Header.Set("Cookie", fmt.Sprintf("%v=%v", flashCookieName, base64.URLEncoding.EncodeToString([]byte(message))))
msg := getFlashMessage(rr, req) msg := getFlashMessage(rr, req)
assert.Equal(t, message, msg) assert.Equal(t, message, msg.ErrorString)
req.Header.Set("Cookie", fmt.Sprintf("%v=%v", flashCookieName, "a")) req.Header.Set("Cookie", fmt.Sprintf("%v=%v", flashCookieName, "a"))
msg = getFlashMessage(rr, req) msg = getFlashMessage(rr, req)
assert.Empty(t, msg) assert.Empty(t, msg)

View file

@ -3529,6 +3529,12 @@ func TestI18NErrors(t *testing.T) {
assert.ErrorIs(t, errI18n, util.ErrValidation) assert.ErrorIs(t, errI18n, util.ErrValidation)
assert.Equal(t, err.Error(), errI18n.Error()) assert.Equal(t, err.Error(), errI18n.Error())
assert.Equal(t, util.I18nError500Message, getI18NErrorString(errI18n, "")) assert.Equal(t, util.I18nError500Message, getI18NErrorString(errI18n, ""))
assert.Equal(t, util.I18nError500Message, errI18n.Message)
assert.Equal(t, "{}", errI18n.Args())
var e1 *util.ValidationError
assert.ErrorAs(t, errI18n, &e1)
var e2 *util.I18nError
assert.ErrorAs(t, errI18n, &e2)
err2 := util.NewI18nError(fs.ErrNotExist, util.I18nError500Message) err2 := util.NewI18nError(fs.ErrNotExist, util.I18nError500Message)
assert.ErrorIs(t, err2, &util.I18nError{}) assert.ErrorIs(t, err2, &util.I18nError{})
assert.ErrorIs(t, err2, fs.ErrNotExist) assert.ErrorIs(t, err2, fs.ErrNotExist)
@ -3537,7 +3543,10 @@ func TestI18NErrors(t *testing.T) {
errorString := getI18NErrorString(nil, util.I18nError500Message) errorString := getI18NErrorString(nil, util.I18nError500Message)
assert.Equal(t, util.I18nError500Message, errorString) assert.Equal(t, util.I18nError500Message, errorString)
errI18nWrap := util.NewI18nError(errI18n, util.I18nError404Message) errI18nWrap := util.NewI18nError(errI18n, util.I18nError404Message)
assert.Equal(t, util.I18nError500Message, errI18nWrap.I18nMessage) assert.Equal(t, util.I18nError500Message, errI18nWrap.Message)
errI18n = util.NewI18nError(err, util.I18nError500Message, util.I18nErrorArgs(map[string]any{"a": "b"}))
assert.Equal(t, util.I18nError500Message, errI18n.Message)
assert.Equal(t, `{"a":"b"}`, errI18n.Args())
} }
func isSharedProviderSupported() bool { func isSharedProviderSupported() bool {

View file

@ -233,11 +233,15 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword { if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword {
var err error var err error
if tokenClaims.MustSetTwoFactorAuth { if tokenClaims.MustSetTwoFactorAuth {
protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")
err = util.NewI18nError( err = util.NewI18nError(
util.NewGenericError( util.NewGenericError(
fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v", fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))), protocols)),
util.I18nError2FARequired, util.I18nError2FARequired,
util.I18nErrorArgs(map[string]any{
"val": protocols,
}),
) )
} else { } else {
err = util.NewI18nError( err = util.NewI18nError(

View file

@ -497,7 +497,7 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
defer cancel() defer cancel()
if err = token.refresh(ctx, s.binding.OIDC.oauth2Config, s.binding.OIDC.getVerifier(ctx), r); err != nil { if err = token.refresh(ctx, s.binding.OIDC.oauth2Config, s.binding.OIDC.getVerifier(ctx), r); err != nil {
setFlashMessage(w, r, "Your OpenID token is expired, please log-in again") setFlashMessage(w, r, newFlashMessage("Your OpenID token is expired, please log-in again", util.I18nOIDCTokenExpired))
doRedirect() doRedirect()
return oidcToken{}, errInvalidToken return oidcToken{}, errInvalidToken
} }
@ -507,7 +507,10 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
if isAdmin { if isAdmin {
if !token.isAdmin() { if !token.isAdmin() {
logger.Debug(logSender, "", "oidc token associated with cookie %q is not valid for admin users", token.Cookie) logger.Debug(logSender, "", "oidc token associated with cookie %q is not valid for admin users", token.Cookie)
setFlashMessage(w, r, "Your OpenID token is not valid for the SFTPGo Web Admin UI. Please logout from your OpenID server and log-in as an SFTPGo admin") setFlashMessage(w, r, newFlashMessage(
"Your OpenID token is not valid for the SFTPGo Web Admin UI. Please logout from your OpenID server and log-in as an SFTPGo admin",
util.I18nOIDCTokenInvalidAdmin,
))
doRedirect() doRedirect()
return oidcToken{}, errInvalidToken return oidcToken{}, errInvalidToken
} }
@ -515,7 +518,10 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
} }
if token.isAdmin() { if token.isAdmin() {
logger.Debug(logSender, "", "oidc token associated with cookie %q is valid for admin users", token.Cookie) logger.Debug(logSender, "", "oidc token associated with cookie %q is valid for admin users", token.Cookie)
setFlashMessage(w, r, "Your OpenID token is not valid for the SFTPGo Web Client UI. Please logout from your OpenID server and log-in as an SFTPGo user") setFlashMessage(w, r, newFlashMessage(
"Your OpenID token is not valid for the SFTPGo Web Client UI. Please logout from your OpenID server and log-in as an SFTPGo user",
util.I18nOIDCTokenInvalidUser,
))
doRedirect() doRedirect()
return oidcToken{}, errInvalidToken return oidcToken{}, errInvalidToken
} }
@ -541,7 +547,7 @@ func (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next h
} }
_, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience, util.GetIPFromRemoteAddress(r.RemoteAddr)) _, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil { if err != nil {
setFlashMessage(w, r, "Unable to create cookie") setFlashMessage(w, r, newFlashMessage("Unable to create cookie", util.I18nError500Message))
if audience == tokenAudienceWebAdmin { if audience == tokenAudienceWebAdmin {
http.Redirect(w, r, webAdminLoginPath, http.StatusFound) http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
} else { } else {
@ -610,14 +616,14 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code")) oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil { if err != nil {
logger.Debug(logSender, "", "failed to exchange oidc token: %v", err) logger.Debug(logSender, "", "failed to exchange oidc token: %v", err)
setFlashMessage(w, r, "Failed to exchange OpenID token") setFlashMessage(w, r, newFlashMessage("Failed to exchange OpenID token", util.I18nOIDCErrTokenExchange))
doRedirect() doRedirect()
return return
} }
rawIDToken, ok := oauth2Token.Extra("id_token").(string) rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok { if !ok {
logger.Debug(logSender, "", "no id_token field in OAuth2 OpenID token") logger.Debug(logSender, "", "no id_token field in OAuth2 OpenID token")
setFlashMessage(w, r, "No id_token field in OAuth2 OpenID token") setFlashMessage(w, r, newFlashMessage("No id_token field in OAuth2 OpenID token", util.I18nOIDCTokenInvalid))
doRedirect() doRedirect()
return return
} }
@ -625,14 +631,14 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
idToken, err := s.binding.OIDC.getVerifier(ctx).Verify(ctx, rawIDToken) idToken, err := s.binding.OIDC.getVerifier(ctx).Verify(ctx, rawIDToken)
if err != nil { if err != nil {
logger.Debug(logSender, "", "failed to verify oidc token: %v", err) logger.Debug(logSender, "", "failed to verify oidc token: %v", err)
setFlashMessage(w, r, "Failed to verify OpenID token") setFlashMessage(w, r, newFlashMessage("Failed to verify OpenID token", util.I18nOIDCTokenInvalid))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return
} }
if idToken.Nonce != authReq.Nonce { if idToken.Nonce != authReq.Nonce {
logger.Debug(logSender, "", "oidc authentication nonce did not match") logger.Debug(logSender, "", "oidc authentication nonce did not match")
setFlashMessage(w, r, "OpenID authentication nonce did not match") setFlashMessage(w, r, newFlashMessage("OpenID authentication nonce did not match", util.I18nOIDCTokenInvalid))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return
@ -642,7 +648,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
err = idToken.Claims(&claims) err = idToken.Claims(&claims)
if err != nil { if err != nil {
logger.Debug(logSender, "", "unable to get oidc token claims: %v", err) logger.Debug(logSender, "", "unable to get oidc token claims: %v", err)
setFlashMessage(w, r, "Unable to get OpenID token claims") setFlashMessage(w, r, newFlashMessage("Unable to get OpenID token claims", util.I18nOIDCTokenInvalid))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return
@ -663,7 +669,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
s.binding.OIDC.CustomFields, s.binding.OIDC.getForcedRole(authReq.Audience)) s.binding.OIDC.CustomFields, s.binding.OIDC.getForcedRole(authReq.Audience))
if err != nil { if err != nil {
logger.Debug(logSender, "", "unable to parse oidc token claims: %v", err) logger.Debug(logSender, "", "unable to parse oidc token claims: %v", err)
setFlashMessage(w, r, fmt.Sprintf("Unable to parse OpenID token claims: %v", err)) setFlashMessage(w, r, newFlashMessage(fmt.Sprintf("Unable to parse OpenID token claims: %v", err), util.I18nOIDCTokenInvalid))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return
@ -672,7 +678,9 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
case tokenAudienceWebAdmin: case tokenAudienceWebAdmin:
if !token.isAdmin() { if !token.isAdmin() {
logger.Debug(logSender, "", "wrong oidc token role, the mapped user is not an SFTPGo admin") logger.Debug(logSender, "", "wrong oidc token role, the mapped user is not an SFTPGo admin")
setFlashMessage(w, r, "Wrong OpenID role, the logged in user is not an SFTPGo admin") setFlashMessage(w, r, newFlashMessage(
"Wrong OpenID role, the logged in user is not an SFTPGo admin",
util.I18nOIDCTokenInvalidRoleAdmin))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return
@ -680,7 +688,10 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
case tokenAudienceWebClient: case tokenAudienceWebClient:
if token.isAdmin() { if token.isAdmin() {
logger.Debug(logSender, "", "wrong oidc token role, the mapped user is an SFTPGo admin") logger.Debug(logSender, "", "wrong oidc token role, the mapped user is an SFTPGo admin")
setFlashMessage(w, r, "Wrong OpenID role, the logged in user is an SFTPGo admin") setFlashMessage(w, r, newFlashMessage(
"Wrong OpenID role, the logged in user is an SFTPGo admin",
util.I18nOIDCTokenInvalidRoleUser,
))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return
@ -689,7 +700,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
err = token.getUser(r) err = token.getUser(r)
if err != nil { if err != nil {
logger.Debug(logSender, "", "unable to get the sftpgo user associated with oidc token: %v", err) logger.Debug(logSender, "", "unable to get the sftpgo user associated with oidc token: %v", err)
setFlashMessage(w, r, "Unable to get the user associated with the OpenID token") setFlashMessage(w, r, newFlashMessage("Unable to get the user associated with the OpenID token", util.I18nOIDCErrGetUser))
doRedirect() doRedirect()
doLogout(rawIDToken) doLogout(rawIDToken)
return return

View file

@ -160,12 +160,12 @@ 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, err *util.I18nError, ip string) {
data := loginPage{ data := clientLoginPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: util.I18nLoginTitle, Title: util.I18nLoginTitle,
CurrentURL: webClientLoginPath, CurrentURL: webClientLoginPath,
Error: error, Error: err,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebClient, Branding: s.binding.Branding.WebClient,
FormDisabled: s.binding.isWebClientLoginFormDisabled(), FormDisabled: s.binding.isWebClientLoginFormDisabled(),
@ -198,7 +198,7 @@ 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, util.I18nErrorInvalidForm) s.renderClientChangePasswordPage(w, r, util.NewI18nError(err, 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 {
@ -208,7 +208,7 @@ func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *htt
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, getI18NErrorString(err, util.I18nErrorChangePwdGeneric)) s.renderClientChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))
return return
} }
s.handleWebClientLogout(w, r) s.handleWebClientLogout(w, r)
@ -220,7 +220,8 @@ func (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Reques
http.Redirect(w, r, webAdminSetupPath, http.StatusFound) http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
return return
} }
s.renderClientLoginPage(w, r, getFlashMessage(w, r), util.GetIPFromRemoteAddress(r.RemoteAddr)) msg := getFlashMessage(w, r)
s.renderClientLoginPage(w, r, msg.getI18nError(), util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) {
@ -228,7 +229,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, util.I18nErrorInvalidForm, ipAddr) s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return return
} }
protocol := common.ProtocolHTTP protocol := common.ProtocolHTTP
@ -237,33 +238,35 @@ 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientLoginPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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, util.I18nErrorInvalidCSRF, ipAddr) s.renderClientLoginPage(w, r, util.NewI18nError(err, 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, util.I18nError403Message, ipAddr) s.renderClientLoginPage(w, r, util.NewI18nError(err, 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientLoginPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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, getI18NErrorString(err, util.I18nError403Message), ipAddr) s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message), ipAddr)
return return
} }
@ -272,7 +275,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, getI18NErrorString(err, util.I18nErrorFsGeneric), ipAddr) s.renderClientLoginPage(w, r, util.NewI18nError(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,7 +287,7 @@ 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, util.I18nErrorInvalidForm, ipAddr) s.renderClientResetPwdPage(w, r, util.NewI18nError(err, 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 {
@ -294,18 +297,20 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
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, util.I18nErrorChangePwdNoMatch, ipAddr) s.renderClientResetPwdPage(w, r, util.NewI18nError(
errors.New("the two password fields do not match"),
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, getI18NErrorString(err, util.I18nErrorChangePwdGeneric), ipAddr) s.renderClientResetPwdPage(w, r, util.NewI18nError(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, getI18NErrorString(err, util.I18nErrorDirList403), ipAddr) s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorDirList403), ipAddr)
return return
} }
@ -313,7 +318,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, util.I18nErrorLoginAfterReset, ipAddr) s.renderClientResetPwdPage(w, r, util.NewI18nError(err, 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 +333,18 @@ 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, util.I18nErrorInvalidForm, ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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, util.I18nErrorInvalidCSRF, ipAddr) s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
return return
} }
user, userMerged, err := dataprovider.GetUserVariants(username, "") user, userMerged, err := dataprovider.GetUserVariants(username, "")
@ -346,11 +352,13 @@ 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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) {
s.renderClientTwoFactorPage(w, r, "Two factory authentication is not enabled", ipAddr) s.renderClientTwoFactorPage(w, r, util.NewI18nError(
util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled), ipAddr)
return return
} }
for idx, code := range user.Filters.RecoveryCodes { for idx, code := range user.Filters.RecoveryCodes {
@ -360,7 +368,8 @@ 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
return return
} }
user.Filters.RecoveryCodes[idx].Used = true user.Filters.RecoveryCodes[idx].Used = true
@ -377,7 +386,8 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
} }
} }
handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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 +399,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, util.I18nErrorInvalidForm, ipAddr) s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return return
} }
username := claims.Username username := claims.Username
@ -397,25 +407,26 @@ 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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, util.I18nErrorInvalidCSRF, ipAddr) s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, 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, util.I18n2FADisabled, ipAddr) s.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled), ipAddr)
return return
} }
err = user.Filters.TOTPConfig.Secret.Decrypt() err = user.Filters.TOTPConfig.Secret.Decrypt()
@ -428,7 +439,8 @@ 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, util.I18nErrorInvalidCredentials, ipAddr) s.renderClientTwoFactorPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, 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())
@ -601,7 +613,7 @@ func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request
http.Redirect(w, r, webAdminSetupPath, http.StatusFound) http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
return return
} }
s.renderAdminLoginPage(w, r, getFlashMessage(w, r), util.GetIPFromRemoteAddress(r.RemoteAddr)) s.renderAdminLoginPage(w, r, getFlashMessage(w, r).ErrorString, util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func (s *httpdServer) handleWebAdminLogout(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAdminLogout(w http.ResponseWriter, r *http.Request) {
@ -649,7 +661,8 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *
admin, _, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")), admin, _, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
strings.TrimSpace(r.Form.Get("password")), true) strings.TrimSpace(r.Form.Get("password")), true)
if err != nil { if err != nil {
if e, ok := err.(*util.ValidationError); ok { var e *util.ValidationError
if errors.As(err, &e) {
s.renderResetPwdPage(w, r, e.GetErrorString(), ipAddr) s.renderResetPwdPage(w, r, e.GetErrorString(), ipAddr)
return return
} }
@ -712,7 +725,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
func (s *httpdServer) loginUser( func (s *httpdServer) loginUser(
w http.ResponseWriter, r *http.Request, user *dataprovider.User, connectionID, ipAddr string, w http.ResponseWriter, r *http.Request, user *dataprovider.User, connectionID, ipAddr string,
isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, error, ip string), isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string),
) { ) {
c := jwtTokenClaims{ c := jwtTokenClaims{
Username: user.Username, Username: user.Username,
@ -734,7 +747,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, util.I18nError500Message, ipAddr) errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
return return
} }
if isSecondFactorAuth { if isSecondFactorAuth {

View file

@ -144,7 +144,7 @@ func i18nFsMsg(status int) string {
func getI18NErrorString(err error, fallback string) string { func getI18NErrorString(err error, fallback string) string {
var errI18n *util.I18nError var errI18n *util.I18nError
if errors.As(err, &errI18n) { if errors.As(err, &errI18n) {
return errI18n.I18nMessage return errI18n.Message
} }
return fallback return fallback
} }

View file

@ -2652,7 +2652,8 @@ func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http
} }
err = handleForgotPassword(r, r.Form.Get("username"), true) err = handleForgotPassword(r, r.Form.Get("username"), true)
if err != nil { if err != nil {
if e, ok := err.(*util.ValidationError); ok { var e *util.ValidationError
if errors.As(err, &e) {
s.renderForgotPwdPage(w, r, e.GetErrorString(), ipAddr) s.renderForgotPwdPage(w, r, e.GetErrorString(), ipAddr)
return return
} }

View file

@ -141,7 +141,7 @@ type filesPage struct {
CanDownload bool CanDownload bool
CanShare bool CanShare bool
ShareUploadBaseURL string ShareUploadBaseURL string
Error string Error *util.I18nError
Paths []dirMapping Paths []dirMapping
QuotaUsage *userQuotaUsage QuotaUsage *userQuotaUsage
} }
@ -149,7 +149,7 @@ type filesPage struct {
type shareLoginPage struct { type shareLoginPage struct {
commonBasePage commonBasePage
CurrentURL string CurrentURL string
Error string Error *util.I18nError
CSRFToken string CSRFToken string
Title string Title string
Branding UIBranding Branding UIBranding
@ -168,7 +168,7 @@ type shareUploadPage struct {
type clientMessagePage struct { type clientMessagePage struct {
baseClientPage baseClientPage
Error string Error *util.I18nError
Success string Success string
} }
@ -179,23 +179,24 @@ type clientProfilePage struct {
AllowAPIKeyAuth bool AllowAPIKeyAuth bool
Email string Email string
Description string Description string
Error string Error *util.I18nError
} }
type changeClientPasswordPage struct { type changeClientPasswordPage struct {
baseClientPage baseClientPage
Error string Error *util.I18nError
} }
type clientMFAPage struct { type clientMFAPage struct {
baseClientPage baseClientPage
TOTPConfigs []string TOTPConfigs []string
TOTPConfig dataprovider.UserTOTPConfig TOTPConfig dataprovider.UserTOTPConfig
GenerateTOTPURL string GenerateTOTPURL string
ValidateTOTPURL string ValidateTOTPURL string
SaveTOTPURL string SaveTOTPURL string
RecCodesURL string RecCodesURL string
Protocols []string Protocols []string
RequiredProtocols []string
} }
type clientSharesPage struct { type clientSharesPage struct {
@ -207,10 +208,55 @@ type clientSharesPage struct {
type clientSharePage struct { type clientSharePage struct {
baseClientPage baseClientPage
Share *dataprovider.Share Share *dataprovider.Share
Error string Error *util.I18nError
IsAdd bool IsAdd bool
} }
// TODO: merge with loginPage once the WebAdmin supports localization
type clientLoginPage struct {
commonBasePage
CurrentURL string
Error *util.I18nError
CSRFToken string
AltLoginURL string
AltLoginName string
ForgotPwdURL string
OpenIDLoginURL string
Title string
Branding UIBranding
FormDisabled bool
}
type clientResetPwdPage struct {
commonBasePage
CurrentURL string
Error *util.I18nError
CSRFToken string
LoginURL string
Title string
Branding UIBranding
}
type clientTwoFactorPage struct {
commonBasePage
CurrentURL string
Error *util.I18nError
CSRFToken string
RecoveryURL string
Title string
Branding UIBranding
}
type clientForgotPwdPage struct {
commonBasePage
CurrentURL string
Error *util.I18nError
CSRFToken string
LoginURL string
Title string
Branding UIBranding
}
type userQuotaUsage struct { type userQuotaUsage struct {
QuotaSize int64 QuotaSize int64
QuotaFiles int QuotaFiles int
@ -553,11 +599,11 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
return data return data
} }
func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, error, ip string) { func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := forgotPwdPage{ data := clientForgotPwdPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
CurrentURL: webClientForgotPwdPath, CurrentURL: webClientForgotPwdPath,
Error: error, Error: err,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
LoginURL: webClientLoginPath, LoginURL: webClientLoginPath,
Title: util.I18nForgotPwdTitle, Title: util.I18nForgotPwdTitle,
@ -566,11 +612,11 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.R
renderClientTemplate(w, templateForgotPassword, data) renderClientTemplate(w, templateForgotPassword, data)
} }
func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, error, ip string) { func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := resetPwdPage{ data := clientResetPwdPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
CurrentURL: webClientResetPwdPath, CurrentURL: webClientResetPwdPath,
Error: error, Error: err,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
LoginURL: webClientLoginPath, LoginURL: webClientLoginPath,
Title: util.I18nResetPwdTitle, Title: util.I18nResetPwdTitle,
@ -579,12 +625,12 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Re
renderClientTemplate(w, templateResetPassword, data) renderClientTemplate(w, templateResetPassword, data)
} }
func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) { func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := shareLoginPage{ data := shareLoginPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: util.I18nShareLoginTitle, Title: util.I18nShareLoginTitle,
CurrentURL: r.RequestURI, CurrentURL: r.RequestURI,
Error: error, Error: err,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebClient, Branding: s.binding.Branding.WebClient,
} }
@ -599,13 +645,13 @@ func renderClientTemplate(w http.ResponseWriter, tmplName string, data any) {
} }
func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int, err error, message string) { func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int, err error, message string) {
var errString string var i18nErr *util.I18nError
if err != nil { if err != nil {
errString = getI18NErrorString(err, util.I18nError500Message) i18nErr = util.NewI18nError(err, util.I18nError500Message)
} }
data := clientMessagePage{ data := clientMessagePage{
baseClientPage: s.getBaseClientPageData(title, "", r), baseClientPage: s.getBaseClientPageData(title, "", r),
Error: errString, Error: i18nErr,
Success: message, Success: message,
} }
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
@ -628,16 +674,16 @@ func (s *httpdServer) renderClientForbiddenPage(w http.ResponseWriter, r *http.R
} }
func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) { func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
s.renderClientMessagePage(w, r, util.I18nError400Title, http.StatusNotFound, s.renderClientMessagePage(w, r, util.I18nError404Title, http.StatusNotFound,
util.NewI18nError(err, util.I18nError400Message), "") util.NewI18nError(err, util.I18nError404Message), "")
} }
func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, errorString, ip string) { func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := twoFactorPage{ data := clientTwoFactorPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: pageTwoFactorTitle, Title: pageTwoFactorTitle,
CurrentURL: webClientTwoFactorPath, CurrentURL: webClientTwoFactorPath,
Error: errorString, Error: err,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
RecoveryURL: webClientTwoFactorRecoveryPath, RecoveryURL: webClientTwoFactorRecoveryPath,
Branding: s.binding.Branding.WebClient, Branding: s.binding.Branding.WebClient,
@ -648,12 +694,12 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R
renderClientTemplate(w, templateClientTwoFactor, data) renderClientTemplate(w, templateClientTwoFactor, data)
} }
func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, errorString, ip string) { func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := twoFactorPage{ data := clientTwoFactorPage{
commonBasePage: getCommonBasePage(r), commonBasePage: getCommonBasePage(r),
Title: pageTwoFactorRecoveryTitle, Title: pageTwoFactorRecoveryTitle,
CurrentURL: webClientTwoFactorRecoveryPath, CurrentURL: webClientTwoFactorRecoveryPath,
Error: errorString, Error: err,
CSRFToken: createCSRFToken(ip), CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebClient, Branding: s.binding.Branding.WebClient,
} }
@ -676,6 +722,7 @@ func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request
return return
} }
data.TOTPConfig = user.Filters.TOTPConfig data.TOTPConfig = user.Filters.TOTPConfig
data.RequiredProtocols = user.Filters.TwoFactorAuthProtocols
renderClientTemplate(w, templateClientMFA, data) renderClientTemplate(w, templateClientMFA, data)
} }
@ -698,7 +745,7 @@ func (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request,
} }
func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share, func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,
errorString string, isAdd bool) { err *util.I18nError, isAdd bool) {
currentURL := webClientSharePath currentURL := webClientSharePath
title := util.I18nShareAddTitle title := util.I18nShareAddTitle
if !isAdd { if !isAdd {
@ -708,7 +755,7 @@ func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Re
data := clientSharePage{ data := clientSharePage{
baseClientPage: s.getBaseClientPageData(title, currentURL, r), baseClientPage: s.getBaseClientPageData(title, currentURL, r),
Share: share, Share: share,
Error: errorString, Error: err,
IsAdd: isAdd, IsAdd: isAdd,
} }
@ -736,8 +783,8 @@ func getDirMapping(dirName, baseWebPath string) []dirMapping {
return paths return paths
} }
func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName string,
share dataprovider.Share, err *util.I18nError, share dataprovider.Share,
) { ) {
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse") currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse")
baseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, r) baseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, r)
@ -746,7 +793,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
data := filesPage{ data := filesPage{
baseClientPage: baseData, baseClientPage: baseData,
Error: error, Error: err,
CurrentDir: url.QueryEscape(dirName), CurrentDir: url.QueryEscape(dirName),
DownloadURL: path.Join(baseSharePath, "partial"), DownloadURL: path.Join(baseSharePath, "partial"),
// dirName must be escaped because the router expects the full path as single argument // dirName must be escaped because the router expects the full path as single argument
@ -785,10 +832,11 @@ func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Req
renderClientTemplate(w, templateUploadToShare, data) renderClientTemplate(w, templateUploadToShare, data)
} }
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user *dataprovider.User) { func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName string,
err *util.I18nError, user *dataprovider.User) {
data := filesPage{ data := filesPage{
baseClientPage: s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, r), baseClientPage: s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, r),
Error: error, Error: err,
CurrentDir: url.QueryEscape(dirName), CurrentDir: url.QueryEscape(dirName),
DownloadURL: webClientDownloadZipPath, DownloadURL: webClientDownloadZipPath,
ViewPDFURL: webClientViewPDFPath, ViewPDFURL: webClientViewPDFPath,
@ -808,14 +856,14 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
renderClientTemplate(w, templateClientFiles, data) renderClientTemplate(w, templateClientFiles, data)
} }
func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) { func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
data := clientProfilePage{ data := clientProfilePage{
baseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, r), baseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, r),
Error: error, Error: err,
} }
user, userMerged, err := dataprovider.GetUserVariants(data.LoggedUser.Username, "") user, userMerged, errUser := dataprovider.GetUserVariants(data.LoggedUser.Username, "")
if err != nil { if errUser != nil {
s.renderClientInternalServerErrorPage(w, r, err) s.renderClientInternalServerErrorPage(w, r, errUser)
return return
} }
data.PublicKeys = user.PublicKeys data.PublicKeys = user.PublicKeys
@ -826,10 +874,10 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req
renderClientTemplate(w, templateClientProfile, data) renderClientTemplate(w, templateClientProfile, data)
} }
func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) { func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) {
data := changeClientPasswordPage{ data := changeClientPasswordPage{
baseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, r), baseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, r),
Error: error, Error: err,
} }
renderClientTemplate(w, templateClientChangePwd, data) renderClientTemplate(w, templateClientChangePwd, data)
@ -1023,7 +1071,8 @@ func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request
} }
if err = common.Connections.Add(connection); err != nil { if err = common.Connections.Add(connection); err != nil {
s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), util.I18nError429Message, share) s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)),
util.NewI18nError(err, util.I18nError429Message), share)
return return
} }
defer common.Connections.Remove(connection.GetID()) defer common.Connections.Remove(connection.GetID())
@ -1035,18 +1084,20 @@ func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request
info, err = connection.Stat(name, 1) info, err = connection.Stat(name, 1)
} }
if err != nil { if err != nil {
s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), i18nFsMsg(getRespStatus(err)), share) s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)),
util.NewI18nError(err, i18nFsMsg(getRespStatus(err))), share)
return return
} }
if info.IsDir() { if info.IsDir() {
s.renderSharedFilesPage(w, r, share.GetRelativePath(name), "", share) s.renderSharedFilesPage(w, r, share.GetRelativePath(name), nil, share)
return return
} }
dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck
if status, err := downloadFile(w, r, connection, name, info, false, &share); err != nil { if status, err := downloadFile(w, r, connection, name, info, false, &share); err != nil {
dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
if status > 0 { if status > 0 {
s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), i18nFsMsg(getRespStatus(err)), share) s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)),
util.NewI18nError(err, i18nFsMsg(getRespStatus(err))), share)
} }
} }
} }
@ -1228,11 +1279,11 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
info, err = connection.Stat(name, 0) info, err = connection.Stat(name, 0)
} }
if err != nil { if err != nil {
s.renderFilesPage(w, r, path.Dir(name), i18nFsMsg(getRespStatus(err)), &user) s.renderFilesPage(w, r, path.Dir(name), util.NewI18nError(err, i18nFsMsg(getRespStatus(err))), &user)
return return
} }
if info.IsDir() { if info.IsDir() {
s.renderFilesPage(w, r, name, "", &user) s.renderFilesPage(w, r, name, nil, &user)
return return
} }
if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 { if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 {
@ -1242,7 +1293,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
util.NewI18nError(err, util.I18nError416Message), "") util.NewI18nError(err, util.I18nError416Message), "")
return return
} }
s.renderFilesPage(w, r, path.Dir(name), i18nFsMsg(status), &user) s.renderFilesPage(w, r, path.Dir(name), util.NewI18nError(err, i18nFsMsg(status)), &user)
} }
} }
} }
@ -1365,7 +1416,7 @@ func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Req
} }
} }
s.renderAddUpdateSharePage(w, r, share, "", true) s.renderAddUpdateSharePage(w, r, share, nil, true)
} }
func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) {
@ -1379,7 +1430,7 @@ func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.
share, err := dataprovider.ShareExists(shareID, claims.Username) share, err := dataprovider.ShareExists(shareID, claims.Username)
if err == nil { if err == nil {
share.HideConfidentialData() share.HideConfidentialData()
s.renderAddUpdateSharePage(w, r, &share, "", false) s.renderAddUpdateSharePage(w, r, &share, nil, false)
} else if errors.Is(err, util.ErrNotFound) { } else if errors.Is(err, util.ErrNotFound) {
s.renderClientNotFoundPage(w, r, err) s.renderClientNotFoundPage(w, r, err)
} else { } else {
@ -1396,7 +1447,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
} }
share, err := getShareFromPostFields(r) share, err := getShareFromPostFields(r)
if err != nil { if err != nil {
s.renderAddUpdateSharePage(w, r, share, getI18NErrorString(err, util.I18nError500Message), true) s.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nError500Message), true)
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -1410,24 +1461,39 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
share.Username = claims.Username share.Username = claims.Username
if share.Password == "" { if share.Password == "" {
if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) { if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
s.renderAddUpdateSharePage(w, r, share, util.I18nErrorShareNoPwd, true) s.renderAddUpdateSharePage(w, r, share,
util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
true)
return return
} }
} }
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
if err != nil { if err != nil {
s.renderAddUpdateSharePage(w, r, share, util.I18nErrorGetUser, true) s.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nErrorGetUser), true)
return return
} }
if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil { if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil {
s.renderAddUpdateSharePage(w, r, share, util.I18nErrorShareExpirationOutOfRange, true) s.renderAddUpdateSharePage(w, r, share, util.NewI18nError(
err,
util.I18nErrorShareExpirationOutOfRange,
util.I18nErrorArgs(
map[string]any{
"val": time.Now().Add(24 * time.Hour * time.Duration(user.Filters.MaxSharesExpiration+1)).UnixMilli(),
"formatParams": map[string]string{
"year": "numeric",
"month": "numeric",
"day": "numeric",
},
},
),
), true)
return return
} }
err = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role) err = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role)
if err == nil { if err == nil {
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther) http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
} else { } else {
s.renderAddUpdateSharePage(w, r, share, getI18NErrorString(err, util.I18nErrorShareGeneric), true) s.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nErrorShareGeneric), true)
} }
} }
@ -1449,7 +1515,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
} }
updatedShare, err := getShareFromPostFields(r) updatedShare, err := getShareFromPostFields(r)
if err != nil { if err != nil {
s.renderAddUpdateSharePage(w, r, updatedShare, getI18NErrorString(err, util.I18nError500Message), false) s.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nError500Message), false)
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -1464,24 +1530,39 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
} }
if updatedShare.Password == "" { if updatedShare.Password == "" {
if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) { if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorShareNoPwd, false) s.renderAddUpdateSharePage(w, r, updatedShare,
util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
false)
return return
} }
} }
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
if err != nil { if err != nil {
s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorGetUser, false) s.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nErrorGetUser), false)
return return
} }
if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil { if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil {
s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorShareExpirationOutOfRange, false) s.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(
err,
util.I18nErrorShareExpirationOutOfRange,
util.I18nErrorArgs(
map[string]any{
"val": time.Now().Add(24 * time.Hour * time.Duration(user.Filters.MaxSharesExpiration+1)).UnixMilli(),
"formatParams": map[string]string{
"year": "numeric",
"month": "numeric",
"day": "numeric",
},
},
),
), false)
return return
} }
err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role) err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role)
if err == nil { if err == nil {
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther) http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
} else { } else {
s.renderAddUpdateSharePage(w, r, updatedShare, getI18NErrorString(err, util.I18nErrorShareGeneric), false) s.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nErrorShareGeneric), false)
} }
} }
@ -1522,19 +1603,19 @@ func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Reque
func (s *httpdServer) handleClientGetProfile(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleClientGetProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderClientProfilePage(w, r, "") s.renderClientProfilePage(w, r, nil)
} }
func (s *httpdServer) handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderClientChangePasswordPage(w, r, "") s.renderClientChangePasswordPage(w, r, nil)
} }
func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
s.renderClientProfilePage(w, r, util.I18nErrorInvalidForm) s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -1549,7 +1630,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
} }
user, userMerged, err := dataprovider.GetUserVariants(claims.Username, "") user, userMerged, err := dataprovider.GetUserVariants(claims.Username, "")
if err != nil { if err != nil {
s.renderClientProfilePage(w, r, util.I18nErrorGetUser) s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorGetUser))
return return
} }
if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() { if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() {
@ -1576,7 +1657,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
} }
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr, user.Role) err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr, user.Role)
if err != nil { if err != nil {
s.renderClientProfilePage(w, r, getI18NErrorString(err, util.I18nError500Message)) s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nError500Message))
return return
} }
s.renderClientMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated) s.renderClientMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated)
@ -1589,12 +1670,12 @@ func (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request)
func (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderClientTwoFactorPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr)) s.renderClientTwoFactorPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderClientTwoFactorRecoveryPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr)) s.renderClientTwoFactorRecoveryPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) { func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
@ -1646,7 +1727,7 @@ func (s *httpdServer) handleWebClientForgotPwd(w http.ResponseWriter, r *http.Re
s.renderClientNotFoundPage(w, r, errors.New("this page does not exist")) s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
return return
} }
s.renderClientForgotPwdPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr)) s.renderClientForgotPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
@ -1655,7 +1736,7 @@ func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *htt
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
s.renderClientForgotPwdPage(w, r, util.I18nErrorInvalidForm, ipAddr) s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, 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 {
@ -1665,7 +1746,7 @@ func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *htt
username := strings.TrimSpace(r.Form.Get("username")) username := strings.TrimSpace(r.Form.Get("username"))
err = handleForgotPassword(r, username, false) err = handleForgotPassword(r, username, false)
if err != nil { if err != nil {
s.renderClientForgotPwdPage(w, r, getI18NErrorString(err, util.I18nErrorPwdResetGeneric), ipAddr) s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric), ipAddr)
return return
} }
http.Redirect(w, r, webClientResetPwdPath, http.StatusFound) http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
@ -1677,7 +1758,7 @@ func (s *httpdServer) handleWebClientPasswordReset(w http.ResponseWriter, r *htt
s.renderClientNotFoundPage(w, r, errors.New("this page does not exist")) s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
return return
} }
s.renderClientResetPwdPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr)) s.renderClientResetPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
@ -1780,29 +1861,30 @@ func (s *httpdServer) ensurePDF(w http.ResponseWriter, r *http.Request, name str
func (s *httpdServer) handleClientShareLoginGet(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleClientShareLoginGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
s.renderShareLoginPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr)) s.renderShareLoginPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
} }
func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.renderShareLoginPage(w, r, util.I18nErrorInvalidForm, ipAddr) s.renderShareLoginPage(w, r, util.NewI18nError(err, 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.renderShareLoginPage(w, r, util.I18nErrorInvalidCSRF, ipAddr) s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
return return
} }
shareID := getURLParam(r, "id") shareID := getURLParam(r, "id")
share, err := dataprovider.ShareExists(shareID, "") share, err := dataprovider.ShareExists(shareID, "")
if err != nil { if err != nil {
s.renderShareLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
return return
} }
match, err := share.CheckCredentials(strings.TrimSpace(r.Form.Get("share_password"))) match, err := share.CheckCredentials(strings.TrimSpace(r.Form.Get("share_password")))
if !match || err != nil { if !match || err != nil {
s.renderShareLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) s.renderShareLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
ipAddr)
return return
} }
c := jwtTokenClaims{ c := jwtTokenClaims{
@ -1810,7 +1892,7 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.
} }
err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr) err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr)
if err != nil { if err != nil {
s.renderShareLoginPage(w, r, util.I18nError500Message, ipAddr) s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
return return
} }
next := path.Clean(r.URL.Query().Get("next")) next := path.Clean(r.URL.Query().Get("next"))

View file

@ -15,6 +15,7 @@
package util package util
import ( import (
"encoding/json"
"errors" "errors"
) )
@ -40,6 +41,7 @@ const (
I18nInvalidAuthReqTitle = "title.invalid_auth_request" I18nInvalidAuthReqTitle = "title.invalid_auth_request"
I18nError403Title = "title.error403" I18nError403Title = "title.error403"
I18nError400Title = "title.error400" I18nError400Title = "title.error400"
I18nError404Title = "title.error404"
I18nError416Title = "title.error416" I18nError416Title = "title.error416"
I18nError429Title = "title.error429" I18nError429Title = "title.error429"
I18nError500Title = "title.error500" I18nError500Title = "title.error500"
@ -136,24 +138,48 @@ const (
I18nProfileUpdated = "general.profile_updated" I18nProfileUpdated = "general.profile_updated"
I18nShareLoginOK = "general.share_ok" I18nShareLoginOK = "general.share_ok"
I18n2FADisabled = "2fa.disabled" I18n2FADisabled = "2fa.disabled"
I18nOIDCTokenExpired = "oidc.token_expired"
I18nOIDCTokenInvalidAdmin = "oidc.token_invalid_webadmin"
I18nOIDCTokenInvalidUser = "oidc.token_invalid_webclient"
I18nOIDCErrTokenExchange = "oidc.token_exchange_err"
I18nOIDCTokenInvalid = "oidc.token_invalid"
I18nOIDCTokenInvalidRoleAdmin = "oidc.role_admin_err"
I18nOIDCTokenInvalidRoleUser = "oidc.role_user_err"
I18nOIDCErrGetUser = "oidc.get_user_err"
) )
// NewI18nError returns a I18nError wrappring the provided error // NewI18nError returns a I18nError wrappring the provided error
func NewI18nError(err error, message string) *I18nError { func NewI18nError(err error, message string, options ...I18nErrorOption) *I18nError {
var errI18n *I18nError var errI18n *I18nError
if errors.As(err, &errI18n) { if errors.As(err, &errI18n) {
return errI18n return errI18n
} }
return &I18nError{ errI18n = &I18nError{
err: err, err: err,
I18nMessage: message, Message: message,
args: nil,
}
for _, opt := range options {
opt(errI18n)
}
return errI18n
}
// I18nErrorOption defines a functional option type that allows to configure the I18nError.
type I18nErrorOption func(*I18nError)
// I18nErrorArgs is a functional option to set I18nError arguments.
func I18nErrorArgs(args map[string]any) I18nErrorOption {
return func(e *I18nError) {
e.args = args
} }
} }
// I18nError is an error wrapper that add a message to use for localization. // I18nError is an error wrapper that add a message to use for localization.
type I18nError struct { type I18nError struct {
err error err error
I18nMessage string Message string
args map[string]any
} }
// Error returns the wrapped error string. // Error returns the wrapped error string.
@ -161,6 +187,11 @@ func (e *I18nError) Error() string {
return e.err.Error() return e.err.Error()
} }
// Unwrap returns the underlying error
func (e *I18nError) Unwrap() error {
return e.err
}
// Is reports if target matches // Is reports if target matches
func (e *I18nError) Is(target error) bool { func (e *I18nError) Is(target error) bool {
if errors.Is(e.err, target) { if errors.Is(e.err, target) {
@ -169,3 +200,19 @@ func (e *I18nError) Is(target error) bool {
_, ok := target.(*I18nError) _, ok := target.(*I18nError)
return ok return ok
} }
// HasArgs returns true if the error has i18n args.
func (e *I18nError) HasArgs() bool {
return len(e.args) > 0
}
// Args returns the provided args in JSON format
func (e *I18nError) Args() string {
if len(e.args) > 0 {
data, err := json.Marshal(e.args)
if err == nil {
return string(data)
}
}
return "{}"
}

View file

@ -19,10 +19,11 @@
"download_shared_file": "Download shared file", "download_shared_file": "Download shared file",
"share_access_error": "Unable to access the share", "share_access_error": "Unable to access the share",
"invalid_auth_request": "Invalid authentication request", "invalid_auth_request": "Invalid authentication request",
"error429": "Too Many Requests",
"error403": "Forbidden",
"error400": "Bad Request", "error400": "Bad Request",
"error403": "Forbidden",
"error404": "Not Found",
"error416": "Requested Range Not Satisfiable", "error416": "Requested Range Not Satisfiable",
"error429": "Too Many Requests",
"error500": "Internal Server Error", "error500": "Internal Server Error",
"errorPDF": "Unable to show PDF file", "errorPDF": "Unable to show PDF file",
"error_editor": "Cannot open file editor" "error_editor": "Cannot open file editor"
@ -52,7 +53,7 @@
"reset_pwd_err_generic": "Unexpected error while resetting password", "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", "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", "ip_not_allowed": "Login is not allowed from this IP address",
"two_factor_required": "Two-factor authentication is required, set it up" "two_factor_required": "Set up two-factor authentication, it is required for the following protocols: {{val}}"
}, },
"theme": { "theme": {
"light": "Light", "light": "Light",
@ -281,9 +282,9 @@
"auth_code_invalid": "Failed to validate the provided authentication code", "auth_code_invalid": "Failed to validate the provided authentication code",
"auth_secret_gen_err": "Failed to generate authentication secret", "auth_secret_gen_err": "Failed to generate authentication secret",
"save_err": "Failed to save configuration", "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", "auth_code_required": "The authentication code is required",
"no_protocol": "Please select at least a protocol" "no_protocol": "Please select at least a protocol",
"required_protocols": "The following protocols are required: {{val}}"
}, },
"share": { "share": {
"scope": "Scope", "scope": "Scope",
@ -309,7 +310,7 @@
"max_tokens_invalid": "Invalid max tokens", "max_tokens_invalid": "Invalid max tokens",
"expiration_invalid": "Invalid expiration", "expiration_invalid": "Invalid expiration",
"err_no_password": "You are not allowed to share files/folders without password", "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", "expiration_out_of_range": "Set an expiration date and make sure it is less than or equal to {{- val, datetime}}",
"generic": "Unexpected error saving share", "generic": "Unexpected error saving share",
"path_required": "At least a path is required", "path_required": "At least a path is required",
"path_write_scope": "The write scope requires exactly one path", "path_write_scope": "The write scope requires exactly one path",
@ -366,5 +367,15 @@
"file_pattern_invalid": "Invalid file name pattern filters", "file_pattern_invalid": "Invalid file name pattern filters",
"disable_active_2fa": "Two-factor authentication cannot be disabled for a user with an active configuration", "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" "pwd_change_conflict": "It is not possible to request a password change and at the same time prevent the password from being changed"
},
"oidc": {
"token_expired": "Your OpenID token has expired, please log in again",
"token_invalid_webadmin": "Your OpenID token is not valid for the WebAdmin UI. Log out of your OpenID server and log in to WebAdmin",
"token_invalid_webclient": "Your OpenID token is not valid for the WebClient UI. Log out of your OpenID server and log in to the WebClient",
"token_exchange_err": "Failed to exchange OpenID token",
"token_invalid": "Invalid OpenID token",
"role_admin_err": "Incorrect OpenID role, logged in user is not an administrator",
"role_user_err": "Incorrect OpenID role, logged in user is an administrator",
"get_user_err": "Failed to get user associated with OpenID token"
} }
} }

View file

@ -19,10 +19,11 @@
"download_shared_file": "Scarica file condiviso", "download_shared_file": "Scarica file condiviso",
"share_access_error": "Impossibile accedere alla condivisione", "share_access_error": "Impossibile accedere alla condivisione",
"invalid_auth_request": "Richiesta di autenticazione non valida", "invalid_auth_request": "Richiesta di autenticazione non valida",
"error429": "Troppe richieste",
"error403": "Non permesso",
"error400": "Richiesta non valida", "error400": "Richiesta non valida",
"error403": "Non permesso",
"error404": "Non trovato",
"error416": "Impossibile tornare l'intervallo richiesto", "error416": "Impossibile tornare l'intervallo richiesto",
"error429": "Troppe richieste",
"error500": "Errore interno del server", "error500": "Errore interno del server",
"errorPDF": "Impossibile mostrare il file PDF", "errorPDF": "Impossibile mostrare il file PDF",
"error_editor": "Impossibile aprire l'editor di file" "error_editor": "Impossibile aprire l'editor di file"
@ -52,7 +53,7 @@
"reset_pwd_err_generic": "Errore imprevisto durante la reimpostazione della password", "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", "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", "ip_not_allowed": "L'accesso non è consentito da questo indirizzo IP",
"two_factor_required": "È richiesta l'autenticazione a due fattori, configurala" "two_factor_required": "Configura l'autenticazione a due fattori, è obbligatoria per i seguenti protocolli: {{val}}"
}, },
"theme": { "theme": {
"light": "Chiaro", "light": "Chiaro",
@ -281,9 +282,9 @@
"auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito", "auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito",
"auth_secret_gen_err": "Impossibile generare il segreto di autenticazione", "auth_secret_gen_err": "Impossibile generare il segreto di autenticazione",
"save_err": "Impossibile salvare la configurazione", "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", "auth_code_required": "Il codice di autenticazione è obbligatorio",
"no_protocol": "Seleziona almeno un protocollo" "no_protocol": "Seleziona almeno un protocollo",
"required_protocols": "I seguenti protocolli sono obbligatori: {{val}}"
}, },
"share": { "share": {
"scope": "Ambito", "scope": "Ambito",
@ -309,7 +310,7 @@
"max_tokens_invalid": "Token massimi non validi", "max_tokens_invalid": "Token massimi non validi",
"expiration_invalid": "Scadenza non valida", "expiration_invalid": "Scadenza non valida",
"err_no_password": "Non sei autorizzato a condividere file/cartelle senza password", "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", "expiration_out_of_range": "Imposta una data di scadenza e assicurati che sia inferiore o uguale al {{- val, datetime}}",
"generic": "Errore imprevisto durante il salvataggio della condivisione", "generic": "Errore imprevisto durante il salvataggio della condivisione",
"path_required": "È necessario almeno un percorso", "path_required": "È necessario almeno un percorso",
"path_write_scope": "L'ambito di scrittura richiede esattamente un percorso", "path_write_scope": "L'ambito di scrittura richiede esattamente un percorso",
@ -366,5 +367,15 @@
"file_pattern_invalid": "Filtri su modelli di nome file non validi", "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", "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" "pwd_change_conflict": "Non è possibile richiedere la modifica della password e allo stesso tempo impedire la modifica della password"
},
"oidc": {
"token_expired": "Il tuo token OpenID è scaduto, effettua nuovamente l'accesso",
"token_invalid_webadmin": "Il tuo token OpenID non è valido per l'interfaccia utente WebAdmin. Esci dal tuo server OpenID e accedi a WebAdmin",
"token_invalid_webclient": "Il tuo token OpenID non è valido per l'interfaccia utente WebClient. Esci dal tuo server OpenID e accedi al WebClient",
"token_exchange_err": "Impossibile scambiare il token OpenID",
"token_invalid": "Token OpenID non valido",
"role_admin_err": "Ruolo OpenID errato, l'utente che ha effettuato l'accesso non è un amministratore",
"role_user_err": "Ruolo OpenID errato, l'utente che ha effettuato l'accesso è un amministratore",
"get_user_err": "Impossibile ottenere l'utente associato al token OpenID"
} }
} }

View file

@ -21,7 +21,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<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 data-i18n="{{.}}" id="errorTxt"></span> <span {{if .}}data-i18n="{{.Message}}" {{if .HasArgs}}data-i18n-options="{{.Args}}"{{end}}{{end}} 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">
@ -202,9 +202,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
} }
function setI18NData(el, value) { function setI18NData(el, value, options) {
el.removeAttr("data-i18n-options");
el.attr("data-i18n", value); el.attr("data-i18n", value);
el.localize(); el.localize(options);
} }
KTUtil.onDOMContentLoaded(function () { KTUtil.onDOMContentLoaded(function () {

View file

@ -151,11 +151,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
//{{- end}} //{{- end}}
$(document).on("i18nload", function(){ $(document).on("i18nload", function(){
let message = 'fs.edit_file';
//{{- if .ReadOnly}} //{{- if .ReadOnly}}
$('#card_title').text($.t('fs.view_file', { path: '{{.Path}}'})); message = 'fs.view_file';
//{{- else}}
$('#card_title').text($.t('fs.edit_file', { path: '{{.Path}}'}));
//{{- end}} //{{- end}}
setI18NData($('#card_title'), message, { path: '{{.Path}}'});
}); });
$(document).on("i18nshow", function(){ $(document).on("i18nshow", function(){

View file

@ -1409,8 +1409,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "fs.delete.err_generic"; errorMessage = "fs.delete.err_generic";
} }
errTxtEl.removeAttr("data-i18n") setI18NData(errTxtEl, errorMessage, {name: itemName});
errTxtEl.text($.t(errorMessage, {name: itemName}));
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }
@ -1484,8 +1483,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "fs.rename.err_generic"; errorMessage = "fs.rename.err_generic";
} }
errTxtEl.removeAttr("data-i18n") setI18NData(errTxtEl, errorMessage, {name: oldName});
errTxtEl.text($.t(errorMessage, {name: oldName}));
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });
} }

View file

@ -41,7 +41,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</span> </span>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{- template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<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 /> <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>

View file

@ -29,7 +29,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
</div> </div>
{{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 data-i18n="[placeholder]login.username" 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 />

View file

@ -53,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">
<span data-i18n="{{.Error}}"></span> <span data-i18n="{{.Error.Message}}" {{if .Error.HasArgs}}data-i18n-options="{{.Error.Args}}"{{end}}></span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -273,8 +273,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- define "extra_js"}} {{- define "extra_js"}}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
const qrModal = new bootstrap.Modal('#qrcode_modal'); const qrModal = new bootstrap.Modal('#qrcode_modal');
const recCodesModal = new bootstrap.Modal('#recovery_codes_modal'); const recCodesModal = new bootstrap.Modal('#recovery_codes_modal');
const requiredProtocols = [];
{{- range .RequiredProtocols}}
requiredProtocols.push('{{.}}');
{{- end}}
function onConfigChanged() { function onConfigChanged() {
let selectedConfig = $('#id_config option:selected').val(); let selectedConfig = $('#id_config option:selected').val();
@ -537,6 +541,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
return; return;
} }
for (let i = 0; i < requiredProtocols.length > 0; i++){
if (!protocolsArray.includes(requiredProtocols[i])){
setI18NData(errTxtEl, '2fa.required_protocols', {val: requiredProtocols.join(', ')});
errDivEl.removeClass("d-none");
return;
}
}
let postData = { let postData = {
protocols: protocolsArray protocols: protocolsArray
} }
@ -587,13 +598,6 @@ 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) {
switch (error.response.status) {
case 400:
errorMessage = "2fa.save_err_proto";
break;
}
}
setI18NData(errTxtEl, errorMessage); setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
}); });

View file

@ -41,7 +41,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</span> </span>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{- template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<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 /> <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>

View file

@ -29,7 +29,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{- template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<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 /> <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>

View file

@ -237,7 +237,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let info = ""; let info = "";
if (row[5] > 0){ if (row[5] > 0){
info+= $.t('share.expiration_date', { info+= $.t('share.expiration_date', {
val: new Date(parseInt(row[5], 10)), val: parseInt(row[5], 10),
formatParams: { formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' }, val: { year: 'numeric', month: 'numeric', day: 'numeric' },
} }
@ -245,7 +245,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
if (row[6] > 0){ if (row[6] > 0){
info+= $.t('share.last_use', { info+= $.t('share.last_use', {
val: new Date(parseInt(row[6], 10)), val: parseInt(row[6], 10),
formatParams: { formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' }, val: { year: 'numeric', month: 'numeric', day: 'numeric' },
} }

View file

@ -29,7 +29,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{- template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<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 /> <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>

View file

@ -29,7 +29,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div> </div>
</div> </div>
</div> </div>
{{template "errmsg" .Error}} {{- template "errmsg" .Error}}
<div class="fv-row mb-10"> <div class="fv-row mb-10">
<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 /> <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>