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 (
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/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
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/mapstructure v1.5.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/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // 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/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 // indirect
google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // 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/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/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
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/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
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/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
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.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
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/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
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-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-20231127180814-3a041ad873d4 h1:W12Pwm4urIbRdGhMEg2NM9O3TWKjNcxQhs46V0ypf/k=
google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4/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-20231127180814-3a041ad873d4/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-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA=
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-20231211222908-989df2bf70f3 h1:kzJAXnzZoFbe5bhZd4zjUuHos/I31yH4thfMb/13oVY=
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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -16,18 +16,47 @@ package httpd
import (
"encoding/base64"
"encoding/json"
"net/http"
"time"
"github.com/drakkan/sftpgo/v2/internal/util"
)
const (
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{
Name: flashCookieName,
Value: base64.URLEncoding.EncodeToString([]byte(value)),
Value: base64.URLEncoding.EncodeToString(value),
Path: "/",
Expires: time.Now().Add(60 * time.Second),
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"`)
}
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)
if err != nil {
return ""
return msg
}
http.SetCookie(w, &http.Cookie{
Name: flashCookieName,
@ -53,9 +83,13 @@ func getFlashMessage(w http.ResponseWriter, r *http.Request) string {
Secure: isTLS(r),
SameSite: http.SameSiteLaxMode,
})
message, err := base64.URLEncoding.DecodeString(cookie.Value)
value, err := base64.URLEncoding.DecodeString(cookie.Value)
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)
require.NoError(t, err)
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))))
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"))
msg = getFlashMessage(rr, req)
assert.Empty(t, msg)

View file

@ -3529,6 +3529,12 @@ func TestI18NErrors(t *testing.T) {
assert.ErrorIs(t, errI18n, util.ErrValidation)
assert.Equal(t, err.Error(), errI18n.Error())
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)
assert.ErrorIs(t, err2, &util.I18nError{})
assert.ErrorIs(t, err2, fs.ErrNotExist)
@ -3537,7 +3543,10 @@ func TestI18NErrors(t *testing.T) {
errorString := getI18NErrorString(nil, util.I18nError500Message)
assert.Equal(t, util.I18nError500Message, errorString)
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 {

View file

@ -233,11 +233,15 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword {
var err error
if tokenClaims.MustSetTwoFactorAuth {
protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")
err = util.NewI18nError(
util.NewGenericError(
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.I18nErrorArgs(map[string]any{
"val": protocols,
}),
)
} else {
err = util.NewI18nError(

View file

@ -497,7 +497,7 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
defer cancel()
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()
return oidcToken{}, errInvalidToken
}
@ -507,7 +507,10 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
if isAdmin {
if !token.isAdmin() {
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()
return oidcToken{}, errInvalidToken
}
@ -515,7 +518,10 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
}
if token.isAdmin() {
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()
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))
if err != nil {
setFlashMessage(w, r, "Unable to create cookie")
setFlashMessage(w, r, newFlashMessage("Unable to create cookie", util.I18nError500Message))
if audience == tokenAudienceWebAdmin {
http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
} 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"))
if err != nil {
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()
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
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()
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)
if err != nil {
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()
doLogout(rawIDToken)
return
}
if idToken.Nonce != authReq.Nonce {
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()
doLogout(rawIDToken)
return
@ -642,7 +648,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
err = idToken.Claims(&claims)
if err != nil {
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()
doLogout(rawIDToken)
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))
if err != nil {
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()
doLogout(rawIDToken)
return
@ -672,7 +678,9 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
case tokenAudienceWebAdmin:
if !token.isAdmin() {
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()
doLogout(rawIDToken)
return
@ -680,7 +688,10 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
case tokenAudienceWebClient:
if token.isAdmin() {
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()
doLogout(rawIDToken)
return
@ -689,7 +700,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
err = token.getUser(r)
if err != nil {
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()
doLogout(rawIDToken)
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) {
data := loginPage{
func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := clientLoginPage{
commonBasePage: getCommonBasePage(r),
Title: util.I18nLoginTitle,
CurrentURL: webClientLoginPath,
Error: error,
Error: err,
CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebClient,
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) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if err := r.ParseForm(); err != nil {
s.renderClientChangePasswordPage(w, r, util.I18nErrorInvalidForm)
s.renderClientChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
return
}
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")),
strings.TrimSpace(r.Form.Get("new_password1")), strings.TrimSpace(r.Form.Get("new_password2")))
if err != nil {
s.renderClientChangePasswordPage(w, r, getI18NErrorString(err, util.I18nErrorChangePwdGeneric))
s.renderClientChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric))
return
}
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)
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) {
@ -228,7 +229,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil {
s.renderClientLoginPage(w, r, util.I18nErrorInvalidForm, ipAddr)
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return
}
protocol := common.ProtocolHTTP
@ -237,33 +238,35 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
if username == "" || password == "" {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
s.renderClientLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderClientLoginPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientLoginPage(w, r, util.I18nErrorInvalidCSRF, ipAddr)
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
return
}
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientLoginPage(w, r, util.I18nError403Message, ipAddr)
s.renderClientLoginPage(w, r, util.NewI18nError(err, util.I18nError403Message), ipAddr)
return
}
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)
if err != nil {
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
}
connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
if err := checkHTTPClientUser(&user, r, connectionID, true); err != nil {
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
}
@ -272,7 +275,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
if err != nil {
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
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
}
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)
err := r.ParseForm()
if err != nil {
s.renderClientResetPwdPage(w, r, util.I18nErrorInvalidForm, ipAddr)
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return
}
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"))
confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password"))
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
}
_, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
newPassword, false)
if err != nil {
s.renderClientResetPwdPage(w, r, getI18NErrorString(err, util.I18nErrorChangePwdGeneric), ipAddr)
s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric), ipAddr)
return
}
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
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
}
@ -313,7 +318,7 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
err = user.CheckFsRoot(connectionID)
if err != nil {
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
}
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)
if err := r.ParseForm(); err != nil {
s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidForm, ipAddr)
s.renderClientTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return
}
username := claims.Username
recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code"))
if username == "" || recoveryCode == "" {
s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
return
}
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
}
user, userMerged, err := dataprovider.GetUserVariants(username, "")
@ -346,11 +352,13 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
if errors.Is(err, util.ErrNotFound) {
handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
}
s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
return
}
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
}
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.Used {
s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderClientTwoFactorRecoveryPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
return
}
user.Filters.RecoveryCodes[idx].Used = true
@ -377,7 +386,8 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
}
}
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) {
@ -389,7 +399,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil {
s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidForm, ipAddr)
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return
}
username := claims.Username
@ -397,25 +407,26 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
if username == "" || passcode == "" {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderClientTwoFactorPage(w, r,
util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCSRF, ipAddr)
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
return
}
user, err := dataprovider.GetUserWithGroupSettings(username, "")
if err != nil {
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
dataprovider.LoginMethodPassword, ipAddr, err)
s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
return
}
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
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
}
err = user.Filters.TOTPConfig.Secret.Decrypt()
@ -428,7 +439,8 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
user.Filters.TOTPConfig.Secret.GetPayload())
if !match || err != nil {
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
}
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)
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) {
@ -649,7 +661,8 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *
admin, _, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
strings.TrimSpace(r.Form.Get("password")), true)
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)
return
}
@ -712,7 +725,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
func (s *httpdServer) loginUser(
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{
Username: user.Username,
@ -734,7 +747,7 @@ func (s *httpdServer) loginUser(
if err != nil {
logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err)
updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
errorFunc(w, r, util.I18nError500Message, ipAddr)
errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
return
}
if isSecondFactorAuth {

View file

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

View file

@ -141,7 +141,7 @@ type filesPage struct {
CanDownload bool
CanShare bool
ShareUploadBaseURL string
Error string
Error *util.I18nError
Paths []dirMapping
QuotaUsage *userQuotaUsage
}
@ -149,7 +149,7 @@ type filesPage struct {
type shareLoginPage struct {
commonBasePage
CurrentURL string
Error string
Error *util.I18nError
CSRFToken string
Title string
Branding UIBranding
@ -168,7 +168,7 @@ type shareUploadPage struct {
type clientMessagePage struct {
baseClientPage
Error string
Error *util.I18nError
Success string
}
@ -179,23 +179,24 @@ type clientProfilePage struct {
AllowAPIKeyAuth bool
Email string
Description string
Error string
Error *util.I18nError
}
type changeClientPasswordPage struct {
baseClientPage
Error string
Error *util.I18nError
}
type clientMFAPage struct {
baseClientPage
TOTPConfigs []string
TOTPConfig dataprovider.UserTOTPConfig
GenerateTOTPURL string
ValidateTOTPURL string
SaveTOTPURL string
RecCodesURL string
Protocols []string
TOTPConfigs []string
TOTPConfig dataprovider.UserTOTPConfig
GenerateTOTPURL string
ValidateTOTPURL string
SaveTOTPURL string
RecCodesURL string
Protocols []string
RequiredProtocols []string
}
type clientSharesPage struct {
@ -207,10 +208,55 @@ type clientSharesPage struct {
type clientSharePage struct {
baseClientPage
Share *dataprovider.Share
Error string
Error *util.I18nError
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 {
QuotaSize int64
QuotaFiles int
@ -553,11 +599,11 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
return data
}
func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, error, ip string) {
data := forgotPwdPage{
func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := clientForgotPwdPage{
commonBasePage: getCommonBasePage(r),
CurrentURL: webClientForgotPwdPath,
Error: error,
Error: err,
CSRFToken: createCSRFToken(ip),
LoginURL: webClientLoginPath,
Title: util.I18nForgotPwdTitle,
@ -566,11 +612,11 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.R
renderClientTemplate(w, templateForgotPassword, data)
}
func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, error, ip string) {
data := resetPwdPage{
func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := clientResetPwdPage{
commonBasePage: getCommonBasePage(r),
CurrentURL: webClientResetPwdPath,
Error: error,
Error: err,
CSRFToken: createCSRFToken(ip),
LoginURL: webClientLoginPath,
Title: util.I18nResetPwdTitle,
@ -579,12 +625,12 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Re
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{
commonBasePage: getCommonBasePage(r),
Title: util.I18nShareLoginTitle,
CurrentURL: r.RequestURI,
Error: error,
Error: err,
CSRFToken: createCSRFToken(ip),
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) {
var errString string
var i18nErr *util.I18nError
if err != nil {
errString = getI18NErrorString(err, util.I18nError500Message)
i18nErr = util.NewI18nError(err, util.I18nError500Message)
}
data := clientMessagePage{
baseClientPage: s.getBaseClientPageData(title, "", r),
Error: errString,
Error: i18nErr,
Success: message,
}
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) {
s.renderClientMessagePage(w, r, util.I18nError400Title, http.StatusNotFound,
util.NewI18nError(err, util.I18nError400Message), "")
s.renderClientMessagePage(w, r, util.I18nError404Title, http.StatusNotFound,
util.NewI18nError(err, util.I18nError404Message), "")
}
func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, errorString, ip string) {
data := twoFactorPage{
func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := clientTwoFactorPage{
commonBasePage: getCommonBasePage(r),
Title: pageTwoFactorTitle,
CurrentURL: webClientTwoFactorPath,
Error: errorString,
Error: err,
CSRFToken: createCSRFToken(ip),
RecoveryURL: webClientTwoFactorRecoveryPath,
Branding: s.binding.Branding.WebClient,
@ -648,12 +694,12 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R
renderClientTemplate(w, templateClientTwoFactor, data)
}
func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, errorString, ip string) {
data := twoFactorPage{
func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
data := clientTwoFactorPage{
commonBasePage: getCommonBasePage(r),
Title: pageTwoFactorRecoveryTitle,
CurrentURL: webClientTwoFactorRecoveryPath,
Error: errorString,
Error: err,
CSRFToken: createCSRFToken(ip),
Branding: s.binding.Branding.WebClient,
}
@ -676,6 +722,7 @@ func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request
return
}
data.TOTPConfig = user.Filters.TOTPConfig
data.RequiredProtocols = user.Filters.TwoFactorAuthProtocols
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,
errorString string, isAdd bool) {
err *util.I18nError, isAdd bool) {
currentURL := webClientSharePath
title := util.I18nShareAddTitle
if !isAdd {
@ -708,7 +755,7 @@ func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Re
data := clientSharePage{
baseClientPage: s.getBaseClientPageData(title, currentURL, r),
Share: share,
Error: errorString,
Error: err,
IsAdd: isAdd,
}
@ -736,8 +783,8 @@ func getDirMapping(dirName, baseWebPath string) []dirMapping {
return paths
}
func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string,
share dataprovider.Share,
func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName string,
err *util.I18nError, share dataprovider.Share,
) {
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse")
baseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, r)
@ -746,7 +793,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
data := filesPage{
baseClientPage: baseData,
Error: error,
Error: err,
CurrentDir: url.QueryEscape(dirName),
DownloadURL: path.Join(baseSharePath, "partial"),
// 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)
}
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{
baseClientPage: s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, r),
Error: error,
Error: err,
CurrentDir: url.QueryEscape(dirName),
DownloadURL: webClientDownloadZipPath,
ViewPDFURL: webClientViewPDFPath,
@ -808,14 +856,14 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
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{
baseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, r),
Error: error,
Error: err,
}
user, userMerged, err := dataprovider.GetUserVariants(data.LoggedUser.Username, "")
if err != nil {
s.renderClientInternalServerErrorPage(w, r, err)
user, userMerged, errUser := dataprovider.GetUserVariants(data.LoggedUser.Username, "")
if errUser != nil {
s.renderClientInternalServerErrorPage(w, r, errUser)
return
}
data.PublicKeys = user.PublicKeys
@ -826,10 +874,10 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req
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{
baseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, r),
Error: error,
Error: err,
}
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 {
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
}
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)
}
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
}
if info.IsDir() {
s.renderSharedFilesPage(w, r, share.GetRelativePath(name), "", share)
s.renderSharedFilesPage(w, r, share.GetRelativePath(name), nil, share)
return
}
dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck
if status, err := downloadFile(w, r, connection, name, info, false, &share); err != nil {
dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
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)
}
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
}
if info.IsDir() {
s.renderFilesPage(w, r, name, "", &user)
s.renderFilesPage(w, r, name, nil, &user)
return
}
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), "")
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) {
@ -1379,7 +1430,7 @@ func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.
share, err := dataprovider.ShareExists(shareID, claims.Username)
if err == nil {
share.HideConfidentialData()
s.renderAddUpdateSharePage(w, r, &share, "", false)
s.renderAddUpdateSharePage(w, r, &share, nil, false)
} else if errors.Is(err, util.ErrNotFound) {
s.renderClientNotFoundPage(w, r, err)
} else {
@ -1396,7 +1447,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
}
share, err := getShareFromPostFields(r)
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
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -1410,24 +1461,39 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
share.Username = claims.Username
if share.Password == "" {
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
}
}
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
if err != nil {
s.renderAddUpdateSharePage(w, r, share, util.I18nErrorGetUser, true)
s.renderAddUpdateSharePage(w, r, share, util.NewI18nError(err, util.I18nErrorGetUser), true)
return
}
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
}
err = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role)
if err == nil {
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
} 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)
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
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -1464,24 +1530,39 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
}
if updatedShare.Password == "" {
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
}
}
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
if err != nil {
s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorGetUser, false)
s.renderAddUpdateSharePage(w, r, updatedShare, util.NewI18nError(err, util.I18nErrorGetUser), false)
return
}
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
}
err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role)
if err == nil {
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
} 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) {
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) {
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) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
s.renderClientProfilePage(w, r, util.I18nErrorInvalidForm)
s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm))
return
}
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, "")
if err != nil {
s.renderClientProfilePage(w, r, util.I18nErrorGetUser)
s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorGetUser))
return
}
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)
if err != nil {
s.renderClientProfilePage(w, r, getI18NErrorString(err, util.I18nError500Message))
s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nError500Message))
return
}
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) {
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) {
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) {
@ -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"))
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) {
@ -1655,7 +1736,7 @@ func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *htt
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
err := r.ParseForm()
if err != nil {
s.renderClientForgotPwdPage(w, r, util.I18nErrorInvalidForm, ipAddr)
s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return
}
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"))
err = handleForgotPassword(r, username, false)
if err != nil {
s.renderClientForgotPwdPage(w, r, getI18NErrorString(err, util.I18nErrorPwdResetGeneric), ipAddr)
s.renderClientForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric), ipAddr)
return
}
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"))
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) {
@ -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) {
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) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := r.ParseForm(); err != nil {
s.renderShareLoginPage(w, r, util.I18nErrorInvalidForm, ipAddr)
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
return
}
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
}
shareID := getURLParam(r, "id")
share, err := dataprovider.ShareExists(shareID, "")
if err != nil {
s.renderShareLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
return
}
match, err := share.CheckCredentials(strings.TrimSpace(r.Form.Get("share_password")))
if !match || err != nil {
s.renderShareLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr)
s.renderShareLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
ipAddr)
return
}
c := jwtTokenClaims{
@ -1810,7 +1892,7 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http.
}
err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr)
if err != nil {
s.renderShareLoginPage(w, r, util.I18nError500Message, ipAddr)
s.renderShareLoginPage(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
return
}
next := path.Clean(r.URL.Query().Get("next"))

View file

@ -15,6 +15,7 @@
package util
import (
"encoding/json"
"errors"
)
@ -40,6 +41,7 @@ const (
I18nInvalidAuthReqTitle = "title.invalid_auth_request"
I18nError403Title = "title.error403"
I18nError400Title = "title.error400"
I18nError404Title = "title.error404"
I18nError416Title = "title.error416"
I18nError429Title = "title.error429"
I18nError500Title = "title.error500"
@ -136,24 +138,48 @@ const (
I18nProfileUpdated = "general.profile_updated"
I18nShareLoginOK = "general.share_ok"
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
func NewI18nError(err error, message string) *I18nError {
func NewI18nError(err error, message string, options ...I18nErrorOption) *I18nError {
var errI18n *I18nError
if errors.As(err, &errI18n) {
return errI18n
}
return &I18nError{
err: err,
I18nMessage: message,
errI18n = &I18nError{
err: err,
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.
type I18nError struct {
err error
I18nMessage string
err error
Message string
args map[string]any
}
// Error returns the wrapped error string.
@ -161,6 +187,11 @@ func (e *I18nError) Error() string {
return e.err.Error()
}
// Unwrap returns the underlying error
func (e *I18nError) Unwrap() error {
return e.err
}
// Is reports if target matches
func (e *I18nError) Is(target error) bool {
if errors.Is(e.err, target) {
@ -169,3 +200,19 @@ func (e *I18nError) Is(target error) bool {
_, ok := target.(*I18nError)
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",
"share_access_error": "Unable to access the share",
"invalid_auth_request": "Invalid authentication request",
"error429": "Too Many Requests",
"error403": "Forbidden",
"error400": "Bad Request",
"error403": "Forbidden",
"error404": "Not Found",
"error416": "Requested Range Not Satisfiable",
"error429": "Too Many Requests",
"error500": "Internal Server Error",
"errorPDF": "Unable to show PDF file",
"error_editor": "Cannot open file editor"
@ -52,7 +53,7 @@
"reset_pwd_err_generic": "Unexpected error while resetting password",
"reset_ok_login_error": "The password reset completed successfully but an unexpected error occurred while signing in",
"ip_not_allowed": "Login is not allowed from this IP address",
"two_factor_required": "Two-factor authentication is required, set it up"
"two_factor_required": "Set up two-factor authentication, it is required for the following protocols: {{val}}"
},
"theme": {
"light": "Light",
@ -281,9 +282,9 @@
"auth_code_invalid": "Failed to validate the provided authentication code",
"auth_secret_gen_err": "Failed to generate authentication secret",
"save_err": "Failed to save configuration",
"save_err_proto": "$t(2fa.save_err). Make sure the protocols enabled comply with company policy",
"auth_code_required": "The authentication code is required",
"no_protocol": "Please select at least a protocol"
"no_protocol": "Please select at least a protocol",
"required_protocols": "The following protocols are required: {{val}}"
},
"share": {
"scope": "Scope",
@ -309,7 +310,7 @@
"max_tokens_invalid": "Invalid max tokens",
"expiration_invalid": "Invalid expiration",
"err_no_password": "You are not allowed to share files/folders without password",
"expiration_out_of_range": "Set an expiration date and ensure it complies with company policy, e.g. is not too far in the future",
"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",
"path_required": "At least a path is required",
"path_write_scope": "The write scope requires exactly one path",
@ -366,5 +367,15 @@
"file_pattern_invalid": "Invalid file name pattern filters",
"disable_active_2fa": "Two-factor authentication cannot be disabled for a user with an active configuration",
"pwd_change_conflict": "It is not possible to request a password change and at the same time prevent the password from being changed"
},
"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",
"share_access_error": "Impossibile accedere alla condivisione",
"invalid_auth_request": "Richiesta di autenticazione non valida",
"error429": "Troppe richieste",
"error403": "Non permesso",
"error400": "Richiesta non valida",
"error403": "Non permesso",
"error404": "Non trovato",
"error416": "Impossibile tornare l'intervallo richiesto",
"error429": "Troppe richieste",
"error500": "Errore interno del server",
"errorPDF": "Impossibile mostrare il file PDF",
"error_editor": "Impossibile aprire l'editor di file"
@ -52,7 +53,7 @@
"reset_pwd_err_generic": "Errore imprevisto durante la reimpostazione della password",
"reset_ok_login_error": "La reimpostazione della password è stata completata correttamente ma si è verificato un errore imprevisto durante l'accesso",
"ip_not_allowed": "L'accesso non è consentito da questo indirizzo IP",
"two_factor_required": "È richiesta l'autenticazione a due fattori, configurala"
"two_factor_required": "Configura l'autenticazione a due fattori, è obbligatoria per i seguenti protocolli: {{val}}"
},
"theme": {
"light": "Chiaro",
@ -281,9 +282,9 @@
"auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito",
"auth_secret_gen_err": "Impossibile generare il segreto di autenticazione",
"save_err": "Impossibile salvare la configurazione",
"save_err_proto": "$t(2fa.save_err). Assicurati che i protocolli abilitati siano conformi alla politica aziendale",
"auth_code_required": "Il codice di autenticazione è obbligatorio",
"no_protocol": "Seleziona almeno un protocollo"
"no_protocol": "Seleziona almeno un protocollo",
"required_protocols": "I seguenti protocolli sono obbligatori: {{val}}"
},
"share": {
"scope": "Ambito",
@ -309,7 +310,7 @@
"max_tokens_invalid": "Token massimi non validi",
"expiration_invalid": "Scadenza non valida",
"err_no_password": "Non sei autorizzato a condividere file/cartelle senza password",
"expiration_out_of_range": "Imposta una data di scadenza e assicurati che sia conforme alla politica aziendale, ad es. non è troppo lontan nel futuro",
"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",
"path_required": "È necessario almeno 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",
"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"
},
"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>
</i>
<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>
<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">
@ -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.localize();
el.localize(options);
}
KTUtil.onDOMContentLoaded(function () {

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
</div>
{{template "errmsg" .Error}}
{{- template "errmsg" .Error}}
{{- if not .FormDisabled}}
<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 />

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=" fw-semibold">
<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>

View file

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

View file

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

View file

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

View file

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

View file

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