WIP new WebAdmin: profile, change password, message pages

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-18 19:18:57 +01:00
parent 87451560e3
commit 91802fad3e
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
19 changed files with 359 additions and 393 deletions

12
go.mod
View file

@ -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

24
go.sum
View file

@ -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=

View file

@ -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,
)
}
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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"

View file

@ -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) {

View file

@ -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) {

View file

@ -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

View file

@ -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}}"
}
}

View file

@ -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}}"
}
}

View file

@ -70,6 +70,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class=" fw-semibold">
<div class="fs-4 text-gray-800">
<span data-i18n="{{.Success}}"></span>
{{- if .Text}}
{{.Text}}
{{- end}}
</div>
</div>
</div>

View file

@ -1,62 +0,0 @@
<!--
Copyright (C) 2019 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Change password</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="user_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" autocomplete="new-password" spellcheck="false" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" autocomplete="new-password" spellcheck="false" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" autocomplete="new-password" spellcheck="false" required>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Change my password</button>
</form>
</div>
</div>
{{end}}

View file

@ -1,74 +0,0 @@
<!--
Copyright (C) 2019 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
{{if .LoggedAdmin.Username}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-2 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
{{if .Success}}
<div class="card mb-2 border-left-success">
<div class="card-body">{{.Success}}</div>
</div>
{{end}}
</div>
</div>
{{else}}
<div class="row justify-content-center">
<div class="col-xl-8 col-lg-9 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<div class="row justify-content-center">
<div class="col-lg-9">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">{{.Title}}</h1>
</div>
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
{{if .Success}}
<div class="card mb-4 border-left-success">
<div class="card-body">{{.Success}}</div>
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{end}}

View file

@ -1,68 +1,82 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2023 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
https://keenthemes.com/products/templates-mega-bundle
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">My profile - {{.LoggedAdmin.Username}}</h6>
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="general.my_profile" class="card-title section-title">My profile</h3>
</div>
<div class="card-body">
{{if .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="profile_form" action="{{.CurrentURL}}" method="POST">
{{- template "errmsg" .Error}}
<form id="page_form" action="{{.CurrentURL}}" method="POST">
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<label for="idEmail" data-i18n="general.email" class="col-md-3 col-form-label">Email</label>
<div class="col-md-9">
<input type="text" class="form-control" id="idEmail" name="email" placeholder="" spellcheck="false"
value="{{.Email}}" maxlength="255">
value="{{.Email}}" maxlength="255" autocomplete="off">
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<div class="form-group row mt-10">
<label for="idDescription" data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
<div class="col-md-9">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Description}}" maxlength="255">
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
</small>
<div class="form-group row align-items-center mt-10">
<label data-i18n="general.api_key_auth" class="col-md-3 col-form-label" for="idAllowAPIKeyAuth">API key authentication</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if .AllowAPIKeyAuth}}checked="checked"{{end}}/>
<label data-i18n="general.api_key_auth_help" class="form-check-label fw-semibold text-gray-800" for="idAllowAPIKeyAuth">
Allow to impersonate yourself, in REST API, using an API key
</label>
</div>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
<div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10">
<span data-i18n="general.submit" class="indicator-label">
Submit
</span>
<span data-i18n="general.wait" class="indicator-progress">
Please wait...
<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
</span>
</button>
</div>
</form>
</div>
</div>
{{end}}
{{- end}}
{{- define "extra_js"}}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
KTUtil.onDOMContentLoaded(function () {
$("#page_form").submit(function (event) {
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
});
});
</script>
{{- end}}

View file

@ -138,10 +138,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- define "extra_js"}}
{{- if .LoggedUser.CanManagePublicKeys}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script>
{{- end}}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
KTUtil.onDOMContentLoaded(function () {
//{{- if .LoggedUser.CanManagePublicKeys}}
initRepeater('#public_keys');
initRepeaterItems();
//{{- end}}
$("#page_form").submit(function (event) {
let submitButton = document.querySelector('#form_submit');
@ -150,5 +153,4 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
});
</script>
{{- end}}
{{- end}}