mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
WIP new WebAdmin: profile, change password, message pages
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
87451560e3
commit
91802fad3e
19 changed files with 359 additions and 393 deletions
12
go.mod
12
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
|
||||
|
|
24
go.sum
24
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=
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}"
|
||||
}
|
||||
}
|
|
@ -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}}"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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">×</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}}
|
|
@ -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}}
|
|
@ -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">×</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}}
|
|
@ -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}}
|
Loading…
Reference in a new issue