diff --git a/go.mod b/go.mod index b5904006..5720b78e 100644 --- a/go.mod +++ b/go.mod @@ -136,7 +136,7 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.57 // indirect + github.com/miekg/dns v1.1.58 // indirect 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 @@ -158,11 +158,11 @@ require ( github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.14.0 // indirect diff --git a/go.sum b/go.sum index dae60544..c0b7dd68 100644 --- a/go.sum +++ b/go.sum @@ -290,8 +290,8 @@ github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbW github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mhale/smtpd v0.8.1 h1:O02u8O3eYAGxZCGf4E98WjyB+rA3DVFZtchEialjX4s= github.com/mhale/smtpd v0.8.1/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/minio/sio v0.3.1 h1:d59r5RTHb1OsQaSl1EaTWurzMMDRLA5fgNmjzD4eVu4= github.com/minio/sio v0.3.1/go.mod h1:S0ovgVgc+sTlQyhiXA1ppBLv7REM7TYi5yyq2qL/Y6o= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -409,18 +409,18 @@ go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/internal/dataprovider/admin.go b/internal/dataprovider/admin.go index 0161b7ea..2cda9026 100644 --- a/internal/dataprovider/admin.go +++ b/internal/dataprovider/admin.go @@ -329,7 +329,10 @@ func (a *Admin) validateRecoveryCodes() error { func (a *Admin) validatePermissions() error { a.Permissions = util.RemoveDuplicates(a.Permissions, false) if len(a.Permissions) == 0 { - return util.NewValidationError("please grant some permissions to this admin") + return util.NewI18nError( + util.NewValidationError("please grant some permissions to this admin"), + util.I18nErrorPermissionsRequired, + ) } if util.Contains(a.Permissions, PermAdminAny) { a.Permissions = []string{PermAdminAny} @@ -340,8 +343,14 @@ func (a *Admin) validatePermissions() error { } if a.Role != "" { if util.Contains(forbiddenPermsForRoleAdmins, perm) { - return util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", - strings.Join(forbiddenPermsForRoleAdmins, ","))) + deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",") + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)), + util.I18nErrorRoleAdminPerms, + util.I18nErrorArgs(map[string]any{ + "val": deniedPerms, + }), + ) } } } @@ -359,7 +368,10 @@ func (a *Admin) validateGroups() error { } if g.Options.AddToUsersAs == GroupAddToUsersAsPrimary { if hasPrimary { - return util.NewValidationError("only one primary group is allowed") + return util.NewI18nError( + util.NewValidationError("only one primary group is allowed"), + util.I18nErrorPrimaryGroup, + ) } hasPrimary = true } @@ -370,25 +382,28 @@ func (a *Admin) validateGroups() error { func (a *Admin) validate() error { a.SetEmptySecretsIfNil() if a.Username == "" { - return util.NewValidationError("username is mandatory") + return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired) } if err := checkReservedUsernames(a.Username); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorReservedUsername) } if a.Password == "" { - return util.NewValidationError("please set a password") + return util.NewI18nError(util.NewValidationError("please set a password"), util.I18nErrorPasswordRequired) } if a.hasRedactedSecret() { return util.NewValidationError("cannot save an admin with a redacted secret") } if err := a.Filters.TOTPConfig.validate(a.Username); err != nil { - return err + return util.NewI18nError(err, util.I18nError2FAInvalid) } if err := a.validateRecoveryCodes(); err != nil { return err } if config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Username) { - return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)), + util.I18nErrorInvalidUser, + ) } if err := a.hashPassword(); err != nil { return err @@ -397,13 +412,19 @@ func (a *Admin) validate() error { return err } if a.Email != "" && !util.IsEmailValid(a.Email) { - return util.NewValidationError(fmt.Sprintf("email %q is not valid", a.Email)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("email %q is not valid", a.Email)), + util.I18nErrorInvalidEmail, + ) } a.Filters.AllowList = util.RemoveDuplicates(a.Filters.AllowList, false) for _, IPMask := range a.Filters.AllowList { _, _, err := net.ParseCIDR(IPMask) if err != nil { - return util.NewValidationError(fmt.Sprintf("could not parse allow list entry %q : %v", IPMask, err)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("could not parse allow list entry %q : %v", IPMask, err)), + util.I18nErrorInvalidIPMask, + ) } } diff --git a/internal/httpd/api_admin.go b/internal/httpd/api_admin.go index 3ebf2ac5..b26e2512 100644 --- a/internal/httpd/api_admin.go +++ b/internal/httpd/api_admin.go @@ -289,17 +289,23 @@ func changeAdminPassword(w http.ResponseWriter, r *http.Request) { func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { - return util.NewValidationError("please provide the current password and the new one two times") + return util.NewI18nError( + util.NewValidationError("please provide the current password and the new one two times"), + util.I18nErrorChangePwdRequiredFields, + ) } if newPassword != confirmNewPassword { - return util.NewValidationError("the two password fields do not match") + return util.NewI18nError(util.NewValidationError("the two password fields do not match"), util.I18nErrorChangePwdNoMatch) } if currentPassword == newPassword { - return util.NewValidationError("the new password must be different from the current one") + return util.NewI18nError( + util.NewValidationError("the new password must be different from the current one"), + util.I18nErrorChangePwdNoDifferent, + ) } claims, err := getTokenClaims(r) if err != nil { - return err + return util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken) } admin, err := dataprovider.AdminExists(claims.Username) if err != nil { @@ -307,7 +313,7 @@ func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confir } match, err := admin.CheckPassword(currentPassword) if !match || err != nil { - return util.NewValidationError("current password does not match") + return util.NewI18nError(util.NewValidationError("current password does not match"), util.I18nErrorChangePwdCurrentNoMatch) } admin.Password = newPassword diff --git a/internal/httpd/auth_utils.go b/internal/httpd/auth_utils.go index 31ea7d7c..4650e2f6 100644 --- a/internal/httpd/auth_utils.go +++ b/internal/httpd/auth_utils.go @@ -440,18 +440,21 @@ func verifyOAuth2Token(tokenString, ip string) (string, error) { token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString) if err != nil || token == nil { logger.Debug(logSender, "", "error validating OAuth2 token %q: %v", tokenString, err) - return "", fmt.Errorf("unable to verify OAuth2 state: %v", err) + return "", util.NewI18nError( + fmt.Errorf("unable to verify OAuth2 state: %v", err), + util.I18nOAuth2ErrorVerifyState, + ) } if !util.Contains(token.Audience(), tokenAudienceOAuth2) { logger.Debug(logSender, "", "error validating OAuth2 token audience") - return "", errors.New("invalid OAuth2 state") + return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState) } if tokenValidationMode != tokenValidationNoIPMatch { if !util.Contains(token.Audience(), ip) { logger.Debug(logSender, "", "error validating OAuth2 token IP audience") - return "", errors.New("invalid OAuth2 state") + return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState) } } if val, ok := token.Get(jwt.JwtIDKey); ok { @@ -460,5 +463,5 @@ func verifyOAuth2Token(tokenString, ip string) (string, error) { } } logger.Debug(logSender, "", "jti not found in OAuth2 token") - return "", errors.New("invalid OAuth2 state") + return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState) } diff --git a/internal/httpd/middleware.go b/internal/httpd/middleware.go index b445a710..523b6c59 100644 --- a/internal/httpd/middleware.go +++ b/internal/httpd/middleware.go @@ -17,6 +17,7 @@ package httpd import ( "errors" "fmt" + "io/fs" "net/http" "net/url" "strings" @@ -264,13 +265,14 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler { func (s *httpdServer) requireBuiltinLogin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if isLoggedInWithOIDC(r) { + err := util.NewI18nError( + util.NewGenericError("This feature is not available if you are logged in with OpenID"), + util.I18nErrorNoOIDCFeature, + ) if isWebClientRequest(r) { - s.renderClientForbiddenPage(w, r, util.NewI18nError( - util.NewGenericError("This feature is not available if you are logged in with OpenID"), - util.I18nErrorNoOIDCFeature, - )) + s.renderClientForbiddenPage(w, r, err) } else { - s.renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID") + s.renderForbiddenPage(w, r, err) } return } @@ -295,7 +297,7 @@ func (s *httpdServer) checkPerm(perm string) func(next http.Handler) http.Handle if !tokenClaims.hasPerm(perm) { if isWebRequest(r) { - s.renderForbiddenPage(w, r, "You don't have permission for this action") + s.renderForbiddenPage(w, r, util.NewI18nError(fs.ErrPermission, util.I18nError403Message)) } else { sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden) } diff --git a/internal/httpd/server.go b/internal/httpd/server.go index 2c07fb68..e7c7f29f 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -636,17 +636,17 @@ func (s *httpdServer) handleWebAdminChangePwdPost(w http.ResponseWriter, r *http r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) err := r.ParseForm() if err != nil { - s.renderChangePasswordPage(w, r, err.Error()) + s.renderChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm)) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err = doChangeAdminPassword(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.renderChangePasswordPage(w, r, err.Error()) + s.renderChangePasswordPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric)) return } s.handleWebAdminLogout(w, r) @@ -662,7 +662,7 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r * return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } newPassword := strings.TrimSpace(r.Form.Get("password")) @@ -690,7 +690,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } username := strings.TrimSpace(r.Form.Get("username")) @@ -1149,8 +1149,8 @@ func (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http. util.NewI18nError(errors.New(http.StatusText(http.StatusTooManyRequests)), util.I18nError429Message), "") return } - s.renderMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", http.StatusTooManyRequests, - err, "") + s.renderMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(errors.New(http.StatusText(http.StatusTooManyRequests)), util.I18nError429Message), "") return } sendAPIResponse(w, r, err, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) @@ -1163,7 +1163,7 @@ func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Reque s.renderClientForbiddenPage(w, r, err) return } - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, err) return } sendAPIResponse(w, r, err, "", http.StatusForbidden) diff --git a/internal/httpd/web.go b/internal/httpd/web.go index 0b06c2ac..34e69d29 100644 --- a/internal/httpd/web.go +++ b/internal/httpd/web.go @@ -29,12 +29,6 @@ import ( const ( pageMFATitle = "Two-factor authentication" - page400Title = "Bad request" - page403Title = "Forbidden" - page404Title = "Not found" - page404Body = "The page you are looking for does not exist." - page500Title = "Internal Server Error" - page500Body = "The server is unable to fulfill your request." pageTwoFactorTitle = "Two-Factor authentication" pageTwoFactorRecoveryTitle = "Two-Factor recovery" webDateTimeFormat = "2006-01-02 15:04:05" // YYYY-MM-DD HH:MM:SS @@ -46,6 +40,8 @@ const ( templateTwoFactorRecovery = "twofactor-recovery.html" templateForgotPassword = "forgot-password.html" templateResetPassword = "reset-password.html" + templateChangePwd = "changepassword.html" + templateMessage = "message.html" templateCommonCSS = "sftpgo.css" templateCommonBase = "base.html" templateCommonBaseLogin = "baselogin.html" diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index fd1cd3bd..a1b33592 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -89,14 +89,12 @@ const ( templateRoles = "roles.html" templateRole = "role.html" templateEvents = "events.html" - templateMessage = "message.html" templateStatus = "status.html" templateDefender = "defender.html" templateIPLists = "iplists.html" templateIPList = "iplist.html" templateConfigs = "configs.html" templateProfile = "profile.html" - templateChangePwd = "changepassword.html" templateMaintenance = "maintenance.html" templateMFA = "mfa.html" templateSetup = "adminsetup.html" @@ -106,7 +104,6 @@ const ( pageEventRulesTitle = "Event rules" pageEventActionsTitle = "Event actions" pageRolesTitle = "Roles" - pageProfileTitle = "My profile" pageChangePwdTitle = "Change password" pageMaintenanceTitle = "Maintenance" pageDefenderTitle = "Auto Blocklist" @@ -144,6 +141,7 @@ type basePage struct { EventsURL string ConfigsURL string LogoutURL string + LoginURL string ProfileURL string ChangePwdURL string MFAURL string @@ -236,7 +234,7 @@ type adminPage struct { type profilePage struct { basePage - Error string + Error *util.I18nError AllowAPIKeyAuth bool Email string Description string @@ -244,7 +242,7 @@ type profilePage struct { type changePasswordPage struct { basePage - Error string + Error *util.I18nError } type mfaPage struct { @@ -300,7 +298,7 @@ type setupPage struct { type folderPage struct { basePage Folder vfs.BaseVirtualFolder - Error string + Error *util.I18nError Mode folderPageMode FsWrapper fsWrapper } @@ -370,8 +368,9 @@ type configsPage struct { type messagePage struct { basePage - Error string + Error *util.I18nError Success string + Text string } type userTemplateFields struct { @@ -403,14 +402,14 @@ func loadAdminTemplates(templatesPath string) { filepath.Join(templatesPath, templateAdminDir, templateAdmin), } profilePaths := []string{ - filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), + filepath.Join(templatesPath, templateCommonDir, templateCommonBase), filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateProfile), } changePwdPaths := []string{ - filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), + filepath.Join(templatesPath, templateCommonDir, templateCommonBase), filepath.Join(templatesPath, templateAdminDir, templateBase), - filepath.Join(templatesPath, templateAdminDir, templateChangePwd), + filepath.Join(templatesPath, templateCommonDir, templateChangePwd), } connectionsPaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), @@ -418,9 +417,9 @@ func loadAdminTemplates(templatesPath string) { filepath.Join(templatesPath, templateAdminDir, templateConnections), } messagePaths := []string{ - filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), + filepath.Join(templatesPath, templateCommonDir, templateCommonBase), filepath.Join(templatesPath, templateAdminDir, templateBase), - filepath.Join(templatesPath, templateAdminDir, templateMessage), + filepath.Join(templatesPath, templateCommonDir, templateMessage), } foldersPaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateCommonBase), @@ -685,6 +684,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) EventsURL: webEventsPath, ConfigsURL: webConfigsPath, LogoutURL: webLogoutPath, + LoginURL: webAdminLoginPath, ProfileURL: webAdminProfilePath, ChangePwdURL: webChangeAdminPwdPath, MFAURL: webAdminMFAPath, @@ -718,39 +718,43 @@ func renderAdminTemplate(w http.ResponseWriter, tmplName string, data any) { } } -func (s *httpdServer) renderMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, - err error, message string, +func (s *httpdServer) renderMessagePageWithString(w http.ResponseWriter, r *http.Request, title string, statusCode int, + err error, message, text string, ) { - var errorString string - if body != "" { - errorString = body + " " - } - if err != nil { - errorString += err.Error() - } data := messagePage{ basePage: s.getBasePageData(title, "", r), - Error: errorString, + Error: getI18nError(err), Success: message, + Text: text, } w.WriteHeader(statusCode) renderAdminTemplate(w, templateMessage, data) } +func (s *httpdServer) renderMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int, + err error, message string, +) { + s.renderMessagePageWithString(w, r, title, statusCode, err, message, "") +} + func (s *httpdServer) renderInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) { - s.renderMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "") + s.renderMessagePage(w, r, util.I18nError500Title, http.StatusInternalServerError, + util.NewI18nError(err, util.I18nError500Message), "") } func (s *httpdServer) renderBadRequestPage(w http.ResponseWriter, r *http.Request, err error) { - s.renderMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nError400Title, http.StatusBadRequest, + util.NewI18nError(err, util.I18nError400Message), "") } -func (s *httpdServer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, body string) { - s.renderMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body) +func (s *httpdServer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, err error) { + s.renderMessagePage(w, r, util.I18nError403Title, http.StatusForbidden, + util.NewI18nError(err, util.I18nError403Message), "") } func (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) { - s.renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "") + s.renderMessagePage(w, r, util.I18nError404Title, http.StatusNotFound, + util.NewI18nError(err, util.I18nError404Message), "") } func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) { @@ -822,10 +826,10 @@ func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) { renderAdminTemplate(w, templateMFA, data) } -func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, error string) { +func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, err error) { data := profilePage{ - basePage: s.getBasePageData(pageProfileTitle, webAdminProfilePath, r), - Error: error, + basePage: s.getBasePageData(util.I18nProfileTitle, webAdminProfilePath, r), + Error: getI18nError(err), } admin, err := dataprovider.AdminExists(data.LoggedUser.Username) if err != nil { @@ -839,10 +843,10 @@ func (s *httpdServer) renderProfilePage(w http.ResponseWriter, r *http.Request, renderAdminTemplate(w, templateProfile, data) } -func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) { +func (s *httpdServer) renderChangePasswordPage(w http.ResponseWriter, r *http.Request, err *util.I18nError) { data := changePasswordPage{ basePage: s.getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r), - Error: error, + Error: err, } renderAdminTemplate(w, templateChangePwd, data) @@ -1166,7 +1170,7 @@ func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request } func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, - mode folderPageMode, error string, + mode folderPageMode, err error, ) { var title, currentURL string switch mode { @@ -1185,7 +1189,7 @@ func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, f data := folderPage{ basePage: s.getBasePageData(title, currentURL, r), - Error: error, + Error: getI18nError(err), Folder: folder, Mode: mode, FsWrapper: fsWrapper{ @@ -2676,7 +2680,7 @@ func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err = handleForgotPassword(r, r.Form.Get("username"), true) @@ -2713,34 +2717,34 @@ func (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) func (s *httpdServer) handleWebAdminProfile(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - s.renderProfilePage(w, r, "") + s.renderProfilePage(w, r, nil) } func (s *httpdServer) handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - s.renderChangePasswordPage(w, r, "") + s.renderChangePasswordPage(w, r, nil) } func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) err := r.ParseForm() if err != nil { - s.renderProfilePage(w, r, err.Error()) + s.renderProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm)) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderProfilePage(w, r, "Invalid token claims") + s.renderProfilePage(w, r, util.NewI18nError(err, util.I18nErrorInvalidToken)) return } admin, err := dataprovider.AdminExists(claims.Username) if err != nil { - s.renderProfilePage(w, r, err.Error()) + s.renderProfilePage(w, r, err) return } admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != "" @@ -2748,11 +2752,10 @@ func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.R admin.Description = r.Form.Get("description") err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role) if err != nil { - s.renderProfilePage(w, r, err.Error()) + s.renderProfilePage(w, r, err) return } - s.renderMessagePage(w, r, "Profile updated", "", http.StatusOK, nil, - "Your profile has been successfully updated") + s.renderMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated) } func (s *httpdServer) handleWebMaintenance(w http.ResponseWriter, r *http.Request) { @@ -2764,7 +2767,7 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } err = r.ParseMultipartForm(MaxRestoreSize) @@ -2776,7 +2779,7 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) { ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } restoreMode, err := strconv.Atoi(r.Form.Get("mode")) @@ -2810,7 +2813,7 @@ func (s *httpdServer) handleWebRestore(w http.ResponseWriter, r *http.Request) { return } - s.renderMessagePage(w, r, "Data restored", "", http.StatusOK, nil, "Your backup was successfully restored") + s.renderMessagePage(w, r, util.I18nMaintenanceTitle, http.StatusOK, nil, util.I18nBackupOK) } func (s *httpdServer) handleGetWebAdmins(w http.ResponseWriter, r *http.Request) { @@ -2877,7 +2880,7 @@ func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Reque r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } admin, err := getAdminFromPostFields(r) @@ -2890,7 +2893,7 @@ func (s *httpdServer) handleWebAddAdminPost(w http.ResponseWriter, r *http.Reque } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err = dataprovider.AddAdmin(&admin, claims.Username, ipAddr, claims.Role) @@ -2921,7 +2924,7 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedAdmin.ID = admin.ID @@ -2994,7 +2997,7 @@ func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } data := s.getBasePageData(util.I18nUsersTitle, webUsersPath, r) @@ -3008,7 +3011,7 @@ func (s *httpdServer) handleWebTemplateFolderGet(w http.ResponseWriter, r *http. folder, err := dataprovider.GetFolderByName(name) if err == nil { folder.FsConfig.SetEmptySecrets() - s.renderFolderPage(w, r, folder, folderPageModeTemplate, "") + s.renderFolderPage(w, r, folder, folderPageModeTemplate, nil) } else if errors.Is(err, util.ErrNotFound) { s.renderNotFoundPage(w, r, err) } else { @@ -3016,7 +3019,7 @@ func (s *httpdServer) handleWebTemplateFolderGet(w http.ResponseWriter, r *http. } } else { folder := vfs.BaseVirtualFolder{} - s.renderFolderPage(w, r, folder, folderPageModeTemplate, "") + s.renderFolderPage(w, r, folder, folderPageModeTemplate, nil) } } @@ -3024,20 +3027,20 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } templateFolder := vfs.BaseVirtualFolder{} err = r.ParseMultipartForm(maxRequestSize) if err != nil { - s.renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, util.NewI18nError(err, util.I18nErrorInvalidForm), "") return } defer r.MultipartForm.RemoveAll() //nolint:errcheck ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } @@ -3045,7 +3048,7 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http templateFolder.Description = r.Form.Get("description") fsConfig, err := getFsConfigFromPostFields(r) if err != nil { - s.renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, err, "") return } templateFolder.FsConfig = fsConfig @@ -3057,16 +3060,18 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http for _, tmpl := range foldersFields { f := getFolderFromTemplate(templateFolder, tmpl) if err := dataprovider.ValidateFolder(&f); err != nil { - s.renderMessagePage(w, r, "Folder validation error", fmt.Sprintf("Error validating folder %q", f.Name), - http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, err, "") return } dump.Folders = append(dump.Folders, f) } if len(dump.Folders) == 0 { - s.renderMessagePage(w, r, "No folders defined", "No valid folders defined, unable to complete the requested action", - http.StatusBadRequest, nil, "") + s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, http.StatusBadRequest, + util.NewI18nError( + errors.New("no valid folder defined, unable to complete the requested action"), + util.I18nErrorFolderTemplate, + ), "") return } if r.Form.Get("form_action") == "export_from_template" { @@ -3076,8 +3081,7 @@ func (s *httpdServer) handleWebTemplateFolderPost(w http.ResponseWriter, r *http return } if err = RestoreFolders(dump.Folders, "", 1, 0, claims.Username, ipAddr, claims.Role); err != nil { - s.renderMessagePage(w, r, "Unable to save folders", "Cannot save the defined folders:", - getRespStatus(err), err, "") + s.renderMessagePage(w, r, util.I18nTemplateFolderTitle, getRespStatus(err), err, "") return } http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) @@ -3126,17 +3130,17 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } templateUser, err := getUserFromPostFields(r) if err != nil { - s.renderMessagePage(w, r, "Error parsing user fields", "", http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nTemplateUserTitle, http.StatusBadRequest, err, "") return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } @@ -3147,8 +3151,7 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R for _, tmpl := range userTmplFields { u := getUserFromTemplate(templateUser, tmpl) if err := dataprovider.ValidateUser(&u); err != nil { - s.renderMessagePage(w, r, "User validation error", fmt.Sprintf("Error validating user %q", u.Username), - http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nTemplateUserTitle, http.StatusBadRequest, err, "") return } // to create a template the "manage_system" permission is required, so role admins cannot use @@ -3162,8 +3165,11 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R } if len(dump.Users) == 0 { - s.renderMessagePage(w, r, "No users defined", "No valid users defined, unable to complete the requested action", - http.StatusBadRequest, nil, "") + s.renderMessagePage(w, r, util.I18nTemplateUserTitle, + http.StatusBadRequest, util.NewI18nError( + errors.New("no valid user defined, unable to complete the requested action"), + util.I18nErrorUserTemplate, + ), "") return } if r.Form.Get("form_action") == "export_from_template" { @@ -3173,8 +3179,7 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R return } if err = RestoreUsers(dump.Users, "", 1, 0, claims.Username, ipAddr, claims.Role); err != nil { - s.renderMessagePage(w, r, "Unable to save users", "Cannot save the defined users:", - getRespStatus(err), err, "") + s.renderMessagePage(w, r, util.I18nTemplateUserTitle, getRespStatus(err), err, "") return } http.Redirect(w, r, webUsersPath, http.StatusSeeOther) @@ -3204,7 +3209,7 @@ func (s *httpdServer) handleWebUpdateUserGet(w http.ResponseWriter, r *http.Requ r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } username := getURLParam(r, "username") @@ -3222,7 +3227,7 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } user, err := getUserFromPostFields(r) @@ -3232,7 +3237,7 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } user = getUserFromTemplate(user, userTemplateFields{ @@ -3259,7 +3264,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } username := getURLParam(r, "username") @@ -3278,7 +3283,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedUser.ID = user.ID @@ -3328,7 +3333,7 @@ func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Req r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } connectionStats := common.Connections.GetStats(claims.Role) @@ -3342,27 +3347,27 @@ func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Req func (s *httpdServer) handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - s.renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, "") + s.renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, nil) } func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } folder := vfs.BaseVirtualFolder{} err = r.ParseMultipartForm(maxRequestSize) if err != nil { - s.renderFolderPage(w, r, folder, folderPageModeAdd, err.Error()) + s.renderFolderPage(w, r, folder, folderPageModeAdd, util.NewI18nError(err, util.I18nErrorInvalidForm)) return } defer r.MultipartForm.RemoveAll() //nolint:errcheck ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } folder.MappedPath = strings.TrimSpace(r.Form.Get("mapped_path")) @@ -3370,7 +3375,7 @@ func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Requ folder.Description = r.Form.Get("description") fsConfig, err := getFsConfigFromPostFields(r) if err != nil { - s.renderFolderPage(w, r, folder, folderPageModeAdd, err.Error()) + s.renderFolderPage(w, r, folder, folderPageModeAdd, err) return } folder.FsConfig = fsConfig @@ -3380,7 +3385,7 @@ func (s *httpdServer) handleWebAddFolderPost(w http.ResponseWriter, r *http.Requ if err == nil { http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) } else { - s.renderFolderPage(w, r, folder, folderPageModeAdd, err.Error()) + s.renderFolderPage(w, r, folder, folderPageModeAdd, err) } } @@ -3389,7 +3394,7 @@ func (s *httpdServer) handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Re name := getURLParam(r, "name") folder, err := dataprovider.GetFolderByName(name) if err == nil { - s.renderFolderPage(w, r, folder, folderPageModeUpdate, "") + s.renderFolderPage(w, r, folder, folderPageModeUpdate, nil) } else if errors.Is(err, util.ErrNotFound) { s.renderNotFoundPage(w, r, err) } else { @@ -3401,7 +3406,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } name := getURLParam(r, "name") @@ -3416,19 +3421,19 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R err = r.ParseMultipartForm(maxRequestSize) if err != nil { - s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error()) + s.renderFolderPage(w, r, folder, folderPageModeUpdate, util.NewI18nError(err, util.I18nErrorInvalidForm)) return } defer r.MultipartForm.RemoveAll() //nolint:errcheck ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } fsConfig, err := getFsConfigFromPostFields(r) if err != nil { - s.renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error()) + s.renderFolderPage(w, r, folder, folderPageModeUpdate, err) return } updatedFolder := vfs.BaseVirtualFolder{ @@ -3448,7 +3453,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R err = dataprovider.UpdateFolder(&updatedFolder, folder.Users, folder.Groups, claims.Username, ipAddr, claims.Role) if err != nil { - s.renderFolderPage(w, r, updatedFolder, folderPageModeUpdate, err.Error()) + s.renderFolderPage(w, r, updatedFolder, folderPageModeUpdate, err) return } http.Redirect(w, r, webFoldersPath, http.StatusSeeOther) @@ -3543,7 +3548,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } group, err := getGroupFromPostFields(r) @@ -3553,7 +3558,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err = dataprovider.AddGroup(&group, claims.Username, ipAddr, claims.Role) @@ -3581,7 +3586,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } name := getURLParam(r, "name") @@ -3600,7 +3605,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedGroup.ID = group.ID @@ -3673,7 +3678,7 @@ func (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } action, err := getEventActionFromPostFields(r) @@ -3683,7 +3688,7 @@ func (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } if err = dataprovider.AddEventAction(&action, claims.Username, ipAddr, claims.Role); err != nil { @@ -3710,7 +3715,7 @@ func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *h r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } name := getURLParam(r, "name") @@ -3729,7 +3734,7 @@ func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *h } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedAction.ID = action.ID @@ -3790,7 +3795,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } rule, err := getEventRuleFromPostFields(r) @@ -3801,7 +3806,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) err = verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr) if err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil { @@ -3828,7 +3833,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } name := getURLParam(r, "name") @@ -3847,7 +3852,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedRule.ID = rule.ID @@ -3903,12 +3908,12 @@ func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Reques } claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err = dataprovider.AddRole(&role, claims.Username, ipAddr, claims.Role) @@ -3935,7 +3940,7 @@ func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Req r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } role, err := dataprovider.RoleExists(getURLParam(r, "name")) @@ -3954,7 +3959,7 @@ func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Req } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedRole.ID = role.ID @@ -4017,12 +4022,12 @@ func (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http entry.Type = listType claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err = dataprovider.AddIPListEntry(&entry, claims.Username, ipAddr, claims.Role) @@ -4054,7 +4059,7 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } listType, ipOrNet, err := getIPListPathParams(r) @@ -4077,7 +4082,7 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedEntry.Type = listType @@ -4104,7 +4109,7 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderBadRequestPage(w, r, errors.New("invalid token claims")) + s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } configs, err := dataprovider.GetConfigs() @@ -4119,7 +4124,7 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderForbiddenPage(w, r, err.Error()) + s.renderForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } var configSection int @@ -4159,20 +4164,17 @@ func (s *httpdServer) handleWebConfigsPost(w http.ResponseWriter, r *http.Reques logger.Error(logSender, "", "unable to decrypt SMTP configuration, cannot activate configuration: %v", err) } } - s.renderMessagePage(w, r, "Configurations updated", "", http.StatusOK, nil, - "Configurations has been successfully updated") + s.renderMessagePage(w, r, util.I18nConfigsTitle, http.StatusOK, nil, util.I18nConfigsOK) } func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) stateToken := r.URL.Query().Get("state") - errorTitle := "Unable to complete OAuth2 flow" - successTitle := "OAuth2 flow completed" state, err := verifyOAuth2Token(stateToken, util.GetIPFromRemoteAddress(r.RemoteAddr)) if err != nil { - s.renderMessagePage(w, r, errorTitle, "Invalid auth request:", http.StatusBadRequest, err, "") + s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest, err, "") return } @@ -4180,7 +4182,8 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R pendingAuth, err := oauth2Mgr.getPendingAuth(state) if err != nil { - s.renderMessagePage(w, r, errorTitle, "Unable to validate auth request:", http.StatusInternalServerError, err, "") + s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError, + util.NewI18nError(err, util.I18nOAuth2ErrorValidateState), "") return } oauth2Config := smtp.OAuth2Config{ @@ -4195,7 +4198,8 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R cfg.RedirectURL = pendingAuth.RedirectURL token, err := cfg.Exchange(ctx, r.URL.Query().Get("code")) if err != nil { - s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusInternalServerError, err, "") + s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError, + util.NewI18nError(err, util.I18nOAuth2ErrTokenExchange), "") return } if token.RefreshToken == "" { @@ -4203,11 +4207,12 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R "Some providers only return the token when the user first authorizes. " + "If you have already registered SFTPGo with this user in the past, revoke access and try again. " + "This way you will invalidate the previous token" - s.renderMessagePage(w, r, errorTitle, "Unable to get token:", http.StatusBadRequest, errors.New(errTxt), "") + s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusBadRequest, + util.NewI18nError(errors.New(errTxt), util.I18nOAuth2ErrNoRefreshToken), "") return } - s.renderMessagePage(w, r, successTitle, "", http.StatusOK, nil, - fmt.Sprintf("Copy the following string, without the quotes, into SMTP OAuth2 Token configuration field: %q", token.RefreshToken)) + s.renderMessagePageWithString(w, r, util.I18nOAuth2Title, http.StatusOK, nil, util.I18nOAuth2OK, + fmt.Sprintf("%q", token.RefreshToken)) } func updateSMTPSecrets(newConfigs, currentConfigs *dataprovider.SMTPConfigs) { diff --git a/internal/httpd/webclient.go b/internal/httpd/webclient.go index 6b8f43fd..a08b4266 100644 --- a/internal/httpd/webclient.go +++ b/internal/httpd/webclient.go @@ -45,20 +45,18 @@ import ( ) const ( - templateClientDir = "webclient" - templateClientBase = "base.html" - templateClientFiles = "files.html" - templateClientMessage = "message.html" - templateClientProfile = "profile.html" - templateClientChangePwd = "changepassword.html" - templateClientMFA = "mfa.html" - templateClientEditFile = "editfile.html" - templateClientShare = "share.html" - templateClientShares = "shares.html" - templateClientViewPDF = "viewpdf.html" - templateShareLogin = "sharelogin.html" - templateShareDownload = "sharedownload.html" - templateUploadToShare = "shareupload.html" + templateClientDir = "webclient" + templateClientBase = "base.html" + templateClientFiles = "files.html" + templateClientProfile = "profile.html" + templateClientMFA = "mfa.html" + templateClientEditFile = "editfile.html" + templateClientShare = "share.html" + templateClientShares = "shares.html" + templateClientViewPDF = "viewpdf.html" + templateShareLogin = "sharelogin.html" + templateShareDownload = "sharedownload.html" + templateUploadToShare = "shareupload.html" ) // condResult is the result of an HTTP request precondition check. @@ -167,6 +165,7 @@ type clientMessagePage struct { baseClientPage Error *util.I18nError Success string + Text string } type clientProfilePage struct { @@ -428,7 +427,7 @@ func loadClientTemplates(templatesPath string) { changePwdPaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateCommonBase), filepath.Join(templatesPath, templateClientDir, templateClientBase), - filepath.Join(templatesPath, templateClientDir, templateClientChangePwd), + filepath.Join(templatesPath, templateCommonDir, templateChangePwd), } loginPaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateCommonBase), @@ -438,7 +437,7 @@ func loadClientTemplates(templatesPath string) { messagePaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateCommonBase), filepath.Join(templatesPath, templateClientDir, templateClientBase), - filepath.Join(templatesPath, templateClientDir, templateClientMessage), + filepath.Join(templatesPath, templateCommonDir, templateMessage), } mfaPaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateCommonBase), @@ -505,9 +504,9 @@ func loadClientTemplates(templatesPath string) { clientTemplates[templateClientFiles] = filesTmpl clientTemplates[templateClientProfile] = profileTmpl - clientTemplates[templateClientChangePwd] = changePwdTmpl + clientTemplates[templateChangePwd] = changePwdTmpl clientTemplates[templateCommonLogin] = loginTmpl - clientTemplates[templateClientMessage] = messageTmpl + clientTemplates[templateMessage] = messageTmpl clientTemplates[templateClientMFA] = mfaTmpl clientTemplates[templateTwoFactor] = twoFactorTmpl clientTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl @@ -597,17 +596,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 i18nErr *util.I18nError - if err != nil { - i18nErr = util.NewI18nError(err, util.I18nError500Message) - } data := clientMessagePage{ baseClientPage: s.getBaseClientPageData(title, "", r), - Error: i18nErr, + Error: getI18nError(err), Success: message, } w.WriteHeader(statusCode) - renderClientTemplate(w, templateClientMessage, data) + renderClientTemplate(w, templateMessage, data) } func (s *httpdServer) renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) { @@ -834,7 +829,7 @@ func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *h Error: err, } - renderClientTemplate(w, templateClientChangePwd, data) + renderClientTemplate(w, templateChangePwd, data) } func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) { diff --git a/internal/util/i18n.go b/internal/util/i18n.go index 1098becc..0dd79ad2 100644 --- a/internal/util/i18n.go +++ b/internal/util/i18n.go @@ -54,6 +54,10 @@ const ( I18nAddUserTitle = "title.add_user" I18nUpdateUserTitle = "title.update_user" I18nTemplateUserTitle = "title.template_user" + I18nMaintenanceTitle = "title.maintenance" + I18nConfigsTitle = "title.configs" + I18nOAuth2Title = "title.oauth2_success" + I18nOAuth2ErrorTitle = "title.oauth2_error" I18nErrorSetupInstallCode = "setup.install_code_mismatch" I18nInvalidAuth = "general.invalid_auth_request" I18nError429Message = "general.error429" @@ -80,6 +84,8 @@ const ( I18nErrorChangePwdCurrentNoMatch = "change_pwd.current_no_match" I18nErrorChangePwdRequired = "change_pwd.required" I18nErrorUsernameRequired = "general.username_required" + I18nErrorPasswordRequired = "general.password_required" + I18nErrorPermissionsRequired = "general.permissions_required" I18nErrorGetUser = "general.err_user" I18nErrorPwdResetForbidded = "login.reset_pwd_forbidden" I18nErrorPwdResetNoEmail = "login.reset_pwd_no_email" @@ -191,6 +197,17 @@ const ( I18nTemplateFolderTitle = "title.template_folder" I18nErrorDuplicatedUsername = "general.duplicated_username" I18nErrorDuplicatedName = "general.duplicated_name" + I18nErrorRoleAdminPerms = "admin.role_permissions" + I18nBackupOK = "general.backup_ok" + I18nErrorFolderTemplate = "virtual_folders.template_no_folder" + I18nErrorUserTemplate = "user.template_no_user" + I18nConfigsOK = "general.configs_saved" + I18nOAuth2ErrorVerifyState = "oauth2.auth_verify_error" + I18nOAuth2ErrorValidateState = "oauth2.auth_validation_error" + I18nOAuth2InvalidState = "oauth2.auth_invalid" + I18nOAuth2ErrTokenExchange = "oauth2.token_exchange_err" + I18nOAuth2ErrNoRefreshToken = "oauth2.no_refresh_token" + I18nOAuth2OK = "oauth2.success" ) // NewI18nError returns a I18nError wrappring the provided error diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index cd869d47..9f02bf8e 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -52,7 +52,9 @@ "update_group": "Update group", "add_folder": "Add virtual folder", "update_folder": "Update virtual folder", - "template_folder": "Virtual folder template" + "template_folder": "Virtual folder template", + "oauth2_error": "Unable to complete OAuth2 flow", + "oauth2_success": "OAuth2 flow completed" }, "setup": { "desc": "To start using SFTPGo you need to create an administrator user", @@ -161,6 +163,7 @@ "ip_mask_help": "Comma separated IP/Mask in CIDR format, for example \"192.168.1.0/24,10.8.0.100/32\"", "allowed_ip_mask_invalid": "Invalid allowed IP/Mask", "username_required": "The username is required", + "password_required": "The password is required", "foldername_required": "The folder name is required", "err_user": "Unable to validate your user", "err_protocol_forbidden": "HTTP protocol is not allowed for your user", @@ -213,7 +216,10 @@ "associations": "Associations", "template_placeholders": "The following placeholders are supported", "duplicated_username": "The specified username already exists", - "duplicated_name": "The specified name already exists" + "duplicated_name": "The specified name already exists", + "permissions_required": "Permissions are required", + "backup_ok": "Backup successfully restored", + "configs_saved": "Configurations has been successfully updated" }, "fs": { "view_file": "View file \"{{- path}}\"", @@ -481,7 +487,8 @@ "template_username_placeholder": "replaced with the specified username", "template_password_placeholder": "replaced with the specified password", "template_help1": "Placeholders will be replaced in paths and credentials of the configured storage backend.", - "template_help2": "The generated users can be saved or exported. Exported users can be imported from the \"Maintenance\" section of this SFTPGo instance or another." + "template_help2": "The generated users can be saved or exported. Exported users can be imported from the \"Maintenance\" section of this SFTPGo instance or another.", + "template_no_user": "No valid user defined, unable to complete the requested action" }, "group": { "view_manage": "View and manage groups", @@ -500,7 +507,8 @@ "template_help": "The generated virtual folders can be saved or exported. Exported folders can be imported from the \"Maintenance\" section of this SFTPGo instance or another.", "name": "Virtual folder name", "submit_generate": "Generate and save folders", - "submit_export": "Generate and export folder" + "submit_export": "Generate and export folder", + "template_no_folder": "No valid virtual folder defined, unable to complete the requested action" }, "storage": { "title": "File system", @@ -600,6 +608,14 @@ "role_user_err": "Incorrect OpenID role, logged in user is an administrator", "get_user_err": "Failed to get user associated with OpenID token" }, + "oauth2": { + "auth_verify_error": "Unable to verify OAuth2 code", + "auth_validation_error": "Unable to verify OAuth2 code", + "auth_invalid": "Invalid OAuth2 code", + "token_exchange_err": "Unable to get OAuth2 token from authorization code", + "no_refresh_token": "The OAuth2 provider returned an empty token. Some providers only return the token when the user first authorizes. If you have already registered SFTPGo with this user in the past, revoke access and try again. This way you will invalidate the previous token", + "success": "Copy the following string, without the quotes, into SMTP OAuth2 Token configuration field:" + }, "filters": { "password_strength": "Password strength", "password_strength_help": "Values in the 50-70 range are suggested for common use cases. 0 means disabled, any password will be accepted", @@ -651,5 +667,8 @@ "api_key_auth_help": "Allow to impersonate the user, in REST API, with an API key", "external_auth_cache_time": "External auth cache time", "external_auth_cache_time_help": "Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache" + }, + "admin": { + "role_permissions": "A role admin cannot have the following permissions: {{val}}" } } \ No newline at end of file diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index 2e4c9d59..a210eb3b 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -52,7 +52,9 @@ "update_group": "Aggiorna gruppo", "add_folder": "Aggiungi cartella virtuale", "update_folder": "Aggiorna cartella virtuale", - "template_folder": "Modello cartella virtuale" + "template_folder": "Modello cartella virtuale", + "oauth2_error": "Impossibile completare il flusso OAuth2", + "oauth2_success": "OAuth2 completato" }, "setup": { "desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore", @@ -161,6 +163,7 @@ "ip_mask_help": "IP/reti separate da virgola in formato CIDR, ad esempio \"192.168.1.0/24,10.8.0.100/32\"", "allowed_ip_mask_invalid": "IP/reti permesse non valide", "username_required": "Il nome utente è obbligatorio", + "password_required": "La password è obbligatoria", "foldername_required": "Il nome della cartella è obbligatorio", "err_user": "Errore validazione utente", "err_protocol_forbidden": "Il protocollo HTTP non è consentito per il tuo utente", @@ -213,7 +216,10 @@ "associations": "Associazioni", "template_placeholders": "Sono supportati i seguenti segnaposto", "duplicated_username": "Il nome utente specificato esiste già", - "duplicated_name": "Il nome specificato esiste già" + "duplicated_name": "Il nome specificato esiste già", + "permissions_required": "I permessi sono obbligatori", + "backup_ok": "Backup ripristinato correttamente", + "configs_saved": "Configurazioni aggiornate" }, "fs": { "view_file": "Visualizza file \"{{- path}}\"", @@ -481,7 +487,8 @@ "template_username_placeholder": "sostituito con il nome utente specificato", "template_password_placeholder": "sostituito con la password specificata", "template_help1": "I segnaposto verranno sostituiti nei percorsi e nelle credenziali del backend di archiviazione configurato.", - "template_help2": "Gli utenti generati possono essere salvati o esportati. Gli utenti esportati possono essere importati dalla sezione \"Manutenzione\" di questa istanza SFTPGo o di un'altra." + "template_help2": "Gli utenti generati possono essere salvati o esportati. Gli utenti esportati possono essere importati dalla sezione \"Manutenzione\" di questa istanza SFTPGo o di un'altra.", + "template_no_user": "Nessun utente valido definito. Impossibile completare l'azione richiesta" }, "group": { "view_manage": "Visualizza e gestisci gruppi", @@ -500,7 +507,8 @@ "template_help": "Le cartelle virtuali generate possono essere salvate o esportate. Le cartelle esportate possono essere importate dalla sezione \"Manutenzione\" di questa istanza SFTPGo o di un'altra.", "name": "Nome cartella virtuale", "submit_generate": "Genera e salva cartelle", - "submit_export": "Genera e esporta cartelle" + "submit_export": "Genera e esporta cartelle", + "template_no_folder": "Nessuna cartella virtuale valida definita. Impossibile completare l'azione richiesta" }, "storage": { "title": "File system", @@ -600,6 +608,14 @@ "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" }, + "oauth2": { + "auth_verify_error": "Impossibile verificare il codice OAuth2", + "auth_validation_error": "Impossibile validare il codice OAuth2", + "auth_invalid": "Codice OAuth2 non valido", + "token_exchange_err": "Impossibile ottenere il token OAuth2 dal codice di autorizzazione", + "no_refresh_token": "Il provider OAuth2 ha restituito un token vuoto. Alcuni provider restituiscono il token solo dopo la prima autorizzazione dell'utente. Se hai già registrato SFTPGo con questo utente in passato, revoca l'accesso e riprova. In questo modo invaliderai il token precedente", + "success": "Copia la seguente stringa, senza virgolette, nel campo di configurazione del token SMTP OAuth2:" + }, "filters": { "password_strength": "Sicurezza password", "password_strength_help": "I valori nell'intervallo 50-70 sono suggeriti per i casi d'uso comuni. 0 significa disabilitato, verrà accettata qualsiasi password", @@ -651,5 +667,8 @@ "api_key_auth_help": "Permetti di impersonare l'utente nelle API REST utilizzando una chiave API", "external_auth_cache_time": "Cache per autenticazione esterna", "external_auth_cache_time_help": "Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache" + }, + "admin": { + "role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}" } } \ No newline at end of file diff --git a/templates/webclient/changepassword.html b/templates/common/changepassword.html similarity index 100% rename from templates/webclient/changepassword.html rename to templates/common/changepassword.html diff --git a/templates/webclient/message.html b/templates/common/message.html similarity index 96% rename from templates/webclient/message.html rename to templates/common/message.html index 5972c4ad..f6a8c271 100644 --- a/templates/webclient/message.html +++ b/templates/common/message.html @@ -70,6 +70,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).