mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 09:00:27 +00:00
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:
parent
691133d7c8
commit
61fe7c39a7
26 changed files with 433 additions and 207 deletions
12
go.mod
12
go.mod
|
@ -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
24
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,12 +179,12 @@ 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 {
|
||||
|
@ -196,6 +196,7 @@ type clientMFAPage struct {
|
|||
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"))
|
||||
|
|
|
@ -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{
|
||||
errI18n = &I18nError{
|
||||
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.
|
||||
type I18nError struct {
|
||||
err error
|
||||
I18nMessage string
|
||||
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 "{}"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 () {
|
||||
|
|
|
@ -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(){
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -275,6 +275,10 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<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 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");
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' },
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue