WIP new WebAdmin: IP lists pages

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-24 19:23:15 +01:00
parent d381304136
commit 8180b75ef1
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
17 changed files with 915 additions and 789 deletions

16
go.mod
View file

@ -10,10 +10,10 @@ require (
github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.5
github.com/aws/aws-sdk-go-v2/config v1.26.6
github.com/aws/aws-sdk-go-v2/credentials v1.16.16
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.13
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.14
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2
@ -32,7 +32,7 @@ require (
github.com/go-sql-driver/mysql v1.7.1
github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.5.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-hclog v1.6.2
github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.5
@ -88,7 +88,7 @@ require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // indirect
@ -171,10 +171,10 @@ require (
golang.org/x/tools v0.17.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/grpc v1.60.1 // indirect
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

36
go.sum
View file

@ -37,20 +37,20 @@ github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.13 h1:8Nt4LBUEKV0FxLBO2BmRzDKax3hp2LRMKySMBwL4vMc=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.13/go.mod h1:t5QEDu/FBJJM4kslbQlTSpYtnhoWDNmHSsgQojIxE0o=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.14 h1:ogP1WgyvN/qxPJkgtFMD7G2eKb5p/61Jomx+nIHXUQ4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.14/go.mod h1:nYd/WmIrXlBHW/5QwrZP81/Gz08wKi87nV6EI1kmqx4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 h1:5oE2WzJE56/mVveuDZPJESKlg/00AaS2pY2QZcnxg4M=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10/go.mod h1:FHbKWQtRBYUz4vO5WBWjzMD2by126ny5y/1EoaWoLfI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
@ -91,8 +91,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY=
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/cockroach-go/v2 v2.3.6 h1:Wlv9TzkrG9V7i6u8dEtmXPrBzvfFp+CgJNs696rAajM=
github.com/cockroachdb/cockroach-go/v2 v2.3.6/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8=
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
@ -208,8 +208,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
@ -531,19 +531,19 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s=
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -2827,7 +2827,10 @@ func (p *BoltProvider) addIPListEntry(entry *IPListEntry) error {
return err
}
if e := bucket.Get([]byte(entry.getKey())); e != nil {
return fmt.Errorf("entry %q already exists", entry.IPOrNet)
return util.NewI18nError(
fmt.Errorf("entry %q already exists", entry.IPOrNet),
util.I18nErrorDuplicatedIPNet,
)
}
entry.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
entry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())

View file

@ -151,6 +151,7 @@ const (
const (
fieldUsername = 1
fieldName = 2
fieldIPNet = 3
)
var (

View file

@ -214,7 +214,7 @@ func (e *IPListEntry) validate() error {
// parse as IP
parsed, err := netip.ParseAddr(e.IPOrNet)
if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid IP %q", e.IPOrNet))
return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid IP %q", e.IPOrNet)), util.I18nErrorIpInvalid)
}
if parsed.Is4() {
e.IPOrNet += "/32"
@ -226,7 +226,7 @@ func (e *IPListEntry) validate() error {
}
prefix, err := netip.ParsePrefix(e.IPOrNet)
if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid network %q: %v", e.IPOrNet, err))
return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid network %q: %v", e.IPOrNet, err)), util.I18nErrorNetInvalid)
}
prefix = prefix.Masked()
if prefix.Addr().Is4In6() {
@ -235,7 +235,7 @@ func (e *IPListEntry) validate() error {
// TODO: to remove when the in memory ranger switch to netip
_, _, err = net.ParseCIDR(e.IPOrNet)
if err != nil {
return util.NewValidationError(fmt.Sprintf("invalid network: %v", err))
return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid network: %v", err)), util.I18nErrorNetInvalid)
}
if prefix.Addr().Is4() || prefix.Addr().Is4In6() {
e.IPType = ipTypeV4

View file

@ -2672,7 +2672,10 @@ func (p *MemoryProvider) addIPListEntry(entry *IPListEntry) error {
}
_, err := p.ipListEntryExistsInternal(entry)
if err == nil {
return fmt.Errorf("entry %q already exists", entry.IPOrNet)
return util.NewI18nError(
fmt.Errorf("entry %q already exists", entry.IPOrNet),
util.I18nErrorDuplicatedIPNet,
)
}
entry.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
entry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())

View file

@ -708,7 +708,7 @@ func (p *MySQLProvider) ipListEntryExists(ipOrNet string, listType IPListType) (
}
func (p *MySQLProvider) addIPListEntry(entry *IPListEntry) error {
return sqlCommonAddIPListEntry(entry, p.dbHandle)
return p.normalizeError(sqlCommonAddIPListEntry(entry, p.dbHandle), fieldIPNet)
}
func (p *MySQLProvider) updateIPListEntry(entry *IPListEntry) error {
@ -834,9 +834,14 @@ func (p *MySQLProvider) normalizeError(err error, fieldType int) error {
if errors.As(err, &mysqlErr) {
switch mysqlErr.Number {
case 1062:
message := util.I18nErrorDuplicatedName
if fieldType == fieldUsername {
var message string
switch fieldType {
case fieldUsername:
message = util.I18nErrorDuplicatedUsername
case fieldIPNet:
message = util.I18nErrorDuplicatedIPNet
default:
message = util.I18nErrorDuplicatedName
}
return util.NewI18nError(
fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),

View file

@ -721,7 +721,7 @@ func (p *PGSQLProvider) ipListEntryExists(ipOrNet string, listType IPListType) (
}
func (p *PGSQLProvider) addIPListEntry(entry *IPListEntry) error {
return sqlCommonAddIPListEntry(entry, p.dbHandle)
return p.normalizeError(sqlCommonAddIPListEntry(entry, p.dbHandle), fieldIPNet)
}
func (p *PGSQLProvider) updateIPListEntry(entry *IPListEntry) error {
@ -853,9 +853,14 @@ func (p *PGSQLProvider) normalizeError(err error, fieldType int) error {
if errors.As(err, &pgsqlErr) {
switch pgsqlErr.Code {
case "23505":
message := util.I18nErrorDuplicatedName
if fieldType == fieldUsername {
var message string
switch fieldType {
case fieldUsername:
message = util.I18nErrorDuplicatedUsername
case fieldIPNet:
message = util.I18nErrorDuplicatedIPNet
default:
message = util.I18nErrorDuplicatedName
}
return util.NewI18nError(
fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),

View file

@ -629,7 +629,7 @@ func (p *SQLiteProvider) ipListEntryExists(ipOrNet string, listType IPListType)
}
func (p *SQLiteProvider) addIPListEntry(entry *IPListEntry) error {
return sqlCommonAddIPListEntry(entry, p.dbHandle)
return p.normalizeError(sqlCommonAddIPListEntry(entry, p.dbHandle), fieldIPNet)
}
func (p *SQLiteProvider) updateIPListEntry(entry *IPListEntry) error {
@ -753,9 +753,14 @@ func (p *SQLiteProvider) normalizeError(err error, fieldType int) error {
if e, ok := err.(sqlite3.Error); ok {
switch e.ExtendedCode {
case 1555, 2067:
message := util.I18nErrorDuplicatedName
if fieldType == fieldUsername {
var message string
switch fieldType {
case fieldUsername:
message = util.I18nErrorDuplicatedUsername
case fieldIPNet:
message = util.I18nErrorDuplicatedIPNet
default:
message = util.I18nErrorDuplicatedName
}
return util.NewI18nError(
fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),

View file

@ -102,11 +102,8 @@ const (
pageEventRulesTitle = "Event rules"
pageEventActionsTitle = "Event actions"
pageMaintenanceTitle = "Maintenance"
pageDefenderTitle = "Auto Blocklist"
pageIPListsTitle = "IP Lists"
pageEventsTitle = "Logs"
pageConfigsTitle = "Configurations"
pageSetupTitle = "Create first admin user"
defaultQueryLimit = 1000
inversePatternType = "inverse"
)
@ -259,7 +256,7 @@ type ipListsPage struct {
type ipListPage struct {
basePage
Entry *dataprovider.IPListEntry
Error string
Error *util.I18nError
Mode genericPageMode
}
@ -460,17 +457,17 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateMaintenance),
}
defenderPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateDefender),
}
ipListsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateIPLists),
}
ipListPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateIPList),
}
@ -997,20 +994,20 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
}
func (s *httpdServer) renderIPListPage(w http.ResponseWriter, r *http.Request, entry dataprovider.IPListEntry,
mode genericPageMode, error string,
mode genericPageMode, err error,
) {
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add a new IP List entry"
title = util.I18nAddIPListTitle
currentURL = fmt.Sprintf("%s/%d", webIPListPath, entry.Type)
case genericPageModeUpdate:
title = "Update IP List entry"
title = util.I18nUpdateIPListTitle
currentURL = fmt.Sprintf("%s/%d/%s", webIPListPath, entry.Type, url.PathEscape(entry.IPOrNet))
}
data := ipListPage{
basePage: s.getBasePageData(title, currentURL, r),
Error: error,
Error: getI18nError(err),
Entry: &entry,
Mode: mode,
}
@ -2955,7 +2952,7 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
func (s *httpdServer) handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
data := defenderHostsPage{
basePage: s.getBasePageData(pageDefenderTitle, webDefenderPath, r),
basePage: s.getBasePageData(util.I18nDefenderTitle, webDefenderPath, r),
DefenderHostsURL: webDefenderHostsPath,
}
@ -3986,7 +3983,7 @@ func (s *httpdServer) handleWebIPListsPage(w http.ResponseWriter, r *http.Reques
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
rtlStatus, rtlProtocols := common.Config.GetRateLimitersStatus()
data := ipListsPage{
basePage: s.getBasePageData(pageIPListsTitle, webIPListsPath, r),
basePage: s.getBasePageData(util.I18nIPListsTitle, webIPListsPath, r),
RateLimitersStatus: rtlStatus,
RateLimitersProtocols: strings.Join(rtlProtocols, ", "),
IsAllowListEnabled: common.Config.IsAllowListEnabled(),
@ -4002,7 +3999,7 @@ func (s *httpdServer) handleWebAddIPListEntryGet(w http.ResponseWriter, r *http.
s.renderBadRequestPage(w, r, err)
return
}
s.renderIPListPage(w, r, dataprovider.IPListEntry{Type: listType}, genericPageModeAdd, "")
s.renderIPListPage(w, r, dataprovider.IPListEntry{Type: listType}, genericPageModeAdd, nil)
}
func (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http.Request) {
@ -4014,7 +4011,7 @@ func (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http
}
entry, err := getIPListEntryFromPostFields(r, listType)
if err != nil {
s.renderIPListPage(w, r, entry, genericPageModeAdd, err.Error())
s.renderIPListPage(w, r, entry, genericPageModeAdd, err)
return
}
entry.Type = listType
@ -4030,7 +4027,7 @@ func (s *httpdServer) handleWebAddIPListEntryPost(w http.ResponseWriter, r *http
}
err = dataprovider.AddIPListEntry(&entry, claims.Username, ipAddr, claims.Role)
if err != nil {
s.renderIPListPage(w, r, entry, genericPageModeAdd, err.Error())
s.renderIPListPage(w, r, entry, genericPageModeAdd, err)
return
}
http.Redirect(w, r, webIPListsPath, http.StatusSeeOther)
@ -4045,7 +4042,7 @@ func (s *httpdServer) handleWebUpdateIPListEntryGet(w http.ResponseWriter, r *ht
}
entry, err := dataprovider.IPListEntryExists(ipOrNet, listType)
if err == nil {
s.renderIPListPage(w, r, entry, genericPageModeUpdate, "")
s.renderIPListPage(w, r, entry, genericPageModeUpdate, nil)
} else if errors.Is(err, util.ErrNotFound) {
s.renderNotFoundPage(w, r, err)
} else {
@ -4075,7 +4072,7 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h
}
updatedEntry, err := getIPListEntryFromPostFields(r, listType)
if err != nil {
s.renderIPListPage(w, r, entry, genericPageModeUpdate, err.Error())
s.renderIPListPage(w, r, entry, genericPageModeUpdate, err)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -4087,7 +4084,7 @@ func (s *httpdServer) handleWebUpdateIPListEntryPost(w http.ResponseWriter, r *h
updatedEntry.IPOrNet = ipOrNet
err = dataprovider.UpdateIPListEntry(&updatedEntry, claims.Username, ipAddr, claims.Role)
if err != nil {
s.renderIPListPage(w, r, entry, genericPageModeUpdate, err.Error())
s.renderIPListPage(w, r, entry, genericPageModeUpdate, err)
return
}
http.Redirect(w, r, webIPListsPath, http.StatusSeeOther)

View file

@ -63,6 +63,10 @@ const (
I18nSessionsTitle = "title.connections"
I18nRolesTitle = "title.roles"
I18nAdminsTitle = "title.admins"
I18nIPListsTitle = "title.ip_lists"
I18nAddIPListTitle = "title.add_ip_list"
I18nUpdateIPListTitle = "title.update_ip_list"
I18nDefenderTitle = "title.defender"
I18nErrorSetupInstallCode = "setup.install_code_mismatch"
I18nInvalidAuth = "general.invalid_auth_request"
I18nError429Message = "general.error429"
@ -204,6 +208,7 @@ const (
I18nTemplateFolderTitle = "title.template_folder"
I18nErrorDuplicatedUsername = "general.duplicated_username"
I18nErrorDuplicatedName = "general.duplicated_name"
I18nErrorDuplicatedIPNet = "ip_list.duplicated"
I18nErrorRoleAdminPerms = "admin.role_permissions"
I18nBackupOK = "general.backup_ok"
I18nErrorFolderTemplate = "virtual_folders.template_no_folder"
@ -218,6 +223,8 @@ const (
I18nErrorAdminSelfPerms = "admin.self_permissions"
I18nErrorAdminSelfDisable = "admin.self_disable"
I18nErrorAdminSelfRole = "admin.self_role"
I18nErrorIpInvalid = "ip_list.ip_invalid"
I18nErrorNetInvalid = "ip_list.net_invalid"
)
// NewI18nError returns a I18nError wrappring the provided error

View file

@ -58,7 +58,9 @@
"add_role": "Add role",
"update_role": "Update role",
"add_admin": "Add admin",
"update_admin": "Update admin"
"update_admin": "Update admin",
"add_ip_list": "Add IP list entry",
"update_ip_list": "Update IP list entry"
},
"setup": {
"desc": "To start using SFTPGo you need to create an administrator user",
@ -229,7 +231,10 @@
"members": "Members",
"members_summary": "Users: {{users}}. Admins: {{admins}}",
"status": "Status",
"last_login": "Last login"
"last_login": "Last login",
"previous": "Previous",
"next": "Next",
"type": "Type"
},
"fs": {
"view_file": "View file \"{{- path}}\"",
@ -710,5 +715,31 @@
},
"role": {
"view_manage": "View and manage roles"
},
"ip_list": {
"view_manage": "View and manage IP lists",
"defender_list": "Defender",
"allow_list": "Allow list",
"ratelimiters_safe_list": "Rate limiters safe list",
"ip_net": "IP/Network",
"protocols": "Protocols",
"mode": "Mode",
"any": "Any",
"allow": "Allow",
"deny": "Deny",
"ip_net_help": "IP address or network in CIDR format, example: \"192.168.1.1 or 10.8.0.100/32 or 2001:db8:1234::/48\"",
"ip_invalid": "Invalid IP address",
"net_invalid": "Invalid network",
"duplicated": "The specified IP/network already exists",
"search": "IP/Network or initial part",
"defender_disabled": "Defender disabled in your configuration",
"allow_list_disabled": "Allow list disabled in your configuration",
"ratelimiters_disabled": "Rate limiters disabled in your configuration"
},
"defender": {
"view_manage": "View and manage auto blocklist",
"ip": "IP address",
"ban_time": "Blocked until",
"score": "Score"
}
}

View file

@ -58,7 +58,9 @@
"add_role": "Aggiungi ruolo",
"update_role": "Aggiorna ruolo",
"add_admin": "Aggiungi amministratore",
"update_admin": "Aggiorna amministratore"
"update_admin": "Aggiorna amministratore",
"add_ip_list": "Aggiungi elemento a lista IP",
"update_ip_list": "Aggiorna elemento lista IP"
},
"setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
@ -229,7 +231,10 @@
"members": "Membri",
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}",
"status": "Stato",
"last_login": "Ultimo accesso"
"last_login": "Ultimo accesso",
"previous": "Precedente",
"next": "Successivo",
"type": "Tipo"
},
"fs": {
"view_file": "Visualizza file \"{{- path}}\"",
@ -710,5 +715,31 @@
},
"role": {
"view_manage": "Visualizza e gestisci ruoli"
},
"ip_list": {
"view_manage": "Visualizza e gestisci liste IP",
"defender_list": "Defender",
"allow_list": "Lista IP consentiti",
"ratelimiters_safe_list": "Lista IP esclusi dai rate limiters",
"ip_net": "IP/Rete",
"protocols": "Protocolli",
"mode": "Modalità",
"any": "Qualunque",
"allow": "Permesso",
"deny": "Non permesso",
"ip_net_help": "Indirizzo IP o rete in formato CIDR, ad esempio: \"192.168.1.1 o 10.8.0.100/32 o 2001:db8:1234::/48\"",
"ip_invalid": "Indirizzo IP non valido",
"net_invalid": "Rete non valida",
"duplicated": "L'IP/Rete specificato esiste già",
"search": "IP/Rete o parte iniziale",
"defender_disabled": "Defender disabilitato in configurazione",
"allow_list_disabled": "Lista IP consentiti disabilitata in configurazione",
"ratelimiters_disabled": "Rate limiters disabilitati in configurazione"
},
"defender": {
"view_manage": "Visualizza e gestisci la blocklist automatica",
"ip": "Indirizzo IP",
"ban_time": "Bloccato fino a",
"score": "Punteggio"
}
}

View file

@ -70,7 +70,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
function disconnectAction(connectionID, node) {
function disconnectAction(connectionID, node) {
ModalAlert.fire({
text: $.t('connections.disconnect_confirm'),
icon: "warning",

View file

@ -1,231 +1,284 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2024 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 "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage auto blocklist</h6>
{{- define "page_body"}}
{{- template "errmsg" ""}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="defender.view_manage" class="card-title section-title">View and manage auto blocklis</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<div id="card_body" class="card-body">
<div id="loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack flex-wrap mb-5">
<div class="d-flex align-items-center position-relative my-2">
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
</div>
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
<a href="{{.DefenderURL}}" class="btn btn-primary">
<i class="ki-solid ki-arrows-circle fs-2"></i>
<span data-i18n="general.refresh">Refresh</span>
</a>
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr>
<th>ID</th>
<th>IP</th>
<th>Ban time</th>
<th>Score</th>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th data-i18n="defender.ip">IP</th>
<th data-i18n="defender.ban_time">Blocked until</th>
<th data-i18n="defender.scopre">Score</th>
<th></th>
</tr>
</thead>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table>
</div>
</div>
</div>
{{end}}
{{- end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to remove the selected entry?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{- define "extra_js"}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script type="text/javascript">
function deleteAction(id) {
ModalAlert.fire({
text: $.t('general.delete_confirm_generic'),
icon: "warning",
confirmButtonText: $.t('general.delete_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.DefenderHostsURL}}' + "/" + encodeURIComponent(id);
function deleteAction() {
let table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
let id = table.row({ selected: true }).data()["id"];
let path = '{{.DefenderHostsURL}}' + "/" + fixedEncodeURIComponent(id);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.DefenderURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Unable to delete the selected entry";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
axios.delete(path, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
let errorMessage;
if (error && error.response) {
switch (error.response.status) {
case 403:
errorMessage = "general.delete_error_403";
break;
case 404:
errorMessage = "general.delete_error_404";
break;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
if (!errorMessage){
errorMessage = "general.delete_error_generic";
}
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.refresh = {
text: '<i class="fas fa-sync-alt"></i>',
name: 'refresh',
titleAttr: "Refresh",
action: function (e, dt, node, config) {
location.reload();
}
};
var datatable = function(){
var dt;
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
let table = $('#dataTable').DataTable({
"ajax": {
"url": "{{.DefenderHostsURL}}",
"dataSrc": "",
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "Failed to get auto blocklist";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
var initDatatable = function () {
$('#errorMsg').addClass("d-none");
dt = $('#dataTable').DataTable({
ajax: {
url: "{{.DefenderHostsURL}}",
dataSrc: "",
error: function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
},
"deferRender": true,
"processing": true,
"columns": [
{ "data": "id" },
{ "data": "ip" },
{
"data": "ban_time",
"defaultContent": ""
},
{
"data": "score",
"defaultContent": ""
}
],
"select": {
"style": "single",
"blurable": true
},
"buttons": [],
"lengthChange": false,
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false
columns: [
{
data: "ip",
defaultContent: "",
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "ban_time",
searchable: false,
defaultContent: "",
render: function(data, type, row) {
if (type === 'display') {
if (data){
let parsed = Date.parse(data);
return $.t('general.datetime', {
val: parseInt(parsed, 10),
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },
}
});
}
return ""
}
return data;
}
},
{
data: "score",
defaultContent: 0,
render: function(data, type, row) {
if (data){
return data;
}
return "";
}
},
{
data: "id",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
//{{- if .LoggedUser.HasPermission "manage_defender"}}
return `<div class="d-flex justify-content-end">
<div class="ms-2">
<a href="#" class="btn btn-sm btn-icon btn-light-danger" data-table-action="delete_row">
<i class="ki-solid ki-cross fs-1"></i>
</a>
</div>
</div>`;
//{{- end}}
}
return "";
}
},
],
deferRender: true,
stateSave: true,
stateDuration: 0,
stateLoadParams: function (settings, data) {
if (data.search.search){
const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
}
},
language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('datatable.no_records')
},
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"loadingRecords": "",
"emptyTable": "No records found"
},
"initComplete": function (settings, json) {
{{if .LoggedAdmin.HasPermission "manage_defender"}}
table.button().add(0, 'delete');
{{end}}
table.button().add(0, 'pageLength');
table.button().add(0, 'refresh');
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
},
"order": [[2, 'desc'],[3,'desc']]
});
order: [[0, 'asc']],
initComplete: function(settings, json) {
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
let api = $.fn.dataTable.Api(settings);
api.columns.adjust().draw("page");
drawAction();
}
});
new $.fn.dataTable.FixedHeader(table);
$.fn.dataTable.ext.errMode = 'none';
dt.on('draw', drawAction);
}
{{if .LoggedAdmin.HasPermission "manage_defender"}}
table.on('select deselect', function () {
let selectedRows = table.rows({ selected: true }).count();
table.button('delete:name').enable(selectedRows == 1);
});
{{end}}
function drawAction() {
KTMenu.createInstances();
handleRowActions();
$('#table_body').localize();
}
var handleDatatableActions = function () {
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
filterSearch.off("keyup");
filterSearch.on('keyup', function (e) {
dt.rows().deselect();
dt.search(e.target.value, true, false).draw();
});
}
function handleRowActions() {
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
deleteAction(dt.row(parent).data().id);
});
});
}
return {
init: function () {
initDatatable();
handleDatatableActions();
}
}
}();
$(document).on("i18nshow", function(){
datatable.init();
});
</script>
{{end}}
{{- end}}

View file

@ -1,79 +1,66 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2024 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 "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<!-- Page Heading -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
{{- define "page_body"}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="{{.Title}}" class="card-title section-title"></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}}
{{- template "errmsg" .Error}}
<form id="iplist_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idIPOrNet" class="col-sm-2 col-form-label">IP/Network</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idIPOrNet" name="ipornet" placeholder=""
value="{{.Entry.IPOrNet}}" maxlength="50" autocomplete="nope" aria-describedby="ipOrNetHelpBlock" required {{if eq .Mode 2}}readonly{{end}}>
{{if ne .Mode 2}}
<small id="ipOrNetHelpBlock" class="form-text text-muted">
IP address or network in CIDR format, example: "192.168.1.1 or 10.8.0.100/32 or 2001:db8:1234::/48"
</small>
{{end}}
<label for="idType" data-i18n="general.type" class="col-md-3 col-form-label">Type</label>
<div class="col-md-9">
<input id="idType" type="text" {{if eq .Entry.Type 1}}data-i18n="[value]ip_list.allow_list"{{end}}{{if eq .Entry.Type 2}}data-i18n="[value]ip_list.defender_list"{{end}}{{if eq .Entry.Type 3}}data-i18n="[value]ip_list.ratelimiters_safe_list"{{end}} name="type" maxlength="50"
class="form-control-plaintext readonly-input" readonly />
</div>
</div>
<div class="form-group row">
<label for="idType" class="col-sm-2 col-form-label">Type</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idType" name="type" placeholder=""
value="{{.Entry.Type.AsString}}" maxlength="50" readonly>
<div class="form-group row mt-10">
<label for="idIPOrNet" data-i18n="ip_list.ip_net" class="col-md-3 col-form-label">IP/Network</label>
<div class="col-md-9">
<input id="idIPOrNet" type="text" name="ipornet" value="{{.Entry.IPOrNet}}" maxlength="50" autocomplete="off"
required {{if eq .Mode 2}}class="form-control-plaintext readonly-input" readonly{{else}}class="form-control" aria-describedby="idIPOrNetHelp"{{end}} />
{{- if ne .Mode 2}}
<div id="idIPOrNetHelp" class="form-text" data-i18n="ip_list.ip_net_help"></div>
{{- end}}
</div>
</div>
{{if eq .Entry.Type 2}}
<div class="form-group row">
<label for="idMode" class="col-sm-2 col-form-label">Mode</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idMode" name="mode">
<option value="2" {{if eq .Entry.Mode 2 }}selected{{end}}>Deny</option>
<option value="1" {{if eq .Entry.Mode 1 }}selected{{end}}>Allow</option>
{{- if eq .Entry.Type 2}}
<div class="form-group row mt-10">
<label for="idMode" data-i18n="ip_list.mode" class="col-md-3 col-form-label">Mode</label>
<div class="col-md-9">
<select id="idMode" name="mode" class="form-select" data-control="i18n-select2" data-hide-search="true">
<option value="2" data-i18n="ip_list.deny" {{if eq .Entry.Mode 2 }}selected{{end}}>Deny</option>
<option value="1" data-i18n="ip_list.allow" {{if eq .Entry.Mode 1 }}selected{{end}}>Allow</option>
</select>
</div>
</div>
{{end}}
{{- end}}
<div class="form-group row">
<label for="idProtocols" class="col-sm-2 col-form-label">Protocols</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idProtocols" name="protocols" multiple title="Any">
<div class="form-group row mt-10">
<label for="idProtocols" data-i18n="ip_list.protocols" class="col-md-3 col-form-label">
Protocols
</label>
<div class="col-md-9">
<select id="idProtocols" name="protocols" data-i18n="[data-placeholder]ip_list.any" class="form-select" data-control="i18n-select2" data-close-on-select="false" multiple>
<option value="1" {{if .Entry.HasProtocol "SSH" }}selected{{end}}>SSH</option>
<option value="2" {{if .Entry.HasProtocol "FTP" }}selected{{end}}>FTP</option>
<option value="4" {{if .Entry.HasProtocol "DAV" }}selected{{end}}>DAV</option>
@ -82,26 +69,38 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Note</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Entry.Description}}" maxlength="512" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional note
</small>
<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 id="idDescription" type="text" class="form-control" name="description" value="{{.Entry.Description}}" maxlength="512">
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<div class="col-sm-12 text-right px-0">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">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" name="form_action" value="submit">
<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}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
{{end}}
{{- end}}
{{- define "extra_js"}}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
$(document).on("i18nshow", function(){
$('#iplist_form').submit(function (event) {
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;
});
});
</script>
{{- end}}

View file

@ -1,515 +1,501 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2024 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 "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage IP Lists</h6>
{{- define "page_body"}}
{{- template "errmsg" ""}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="ip_list.view_manage" class="card-title section-title">View and manage IP Lists</h3>
</div>
<div class="card-body">
{{if not .HasDefender}}
<div id="defender-info" class="card mb-3 border-left-info" style="display: none;">
<div class="card-body">Defender disabled in your configuration</div>
<div id="card_body" class="card-body">
<div id="loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
{{end}}
{{if not .IsAllowListEnabled}}
<div id="allowlist-info" class="card mb-3 border-left-info" style="display: none;">
<div class="card-body">Allowlist disabled in your configuration</div>
</div>
{{end}}
{{if not .RateLimitersStatus}}
<div id="ratelimited-info" class="card mb-3 border-left-info" style="display: none;">
<div class="card-body">Ratelimiters disabled in your configuration</div>
</div>
{{end}}
<div class="form-row">
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idListType" name="list_type" onchange="onListChanged(this.value)">
<option value="2">Defender</option>
<option value="1">Allow list</option>
<option value="3">Rate limiters safe list</option>
</select>
</div>
<div class="form-group col-md-5">
</div>
<div class="form-group col-md-4">
<div class="input-group">
<input type="text" class="form-control bg-light border-0" id="idIp" name="ip" placeholder="IP/Network or initial part" aria-describedby="search-button">
<div class="input-group-append">
<button id="search-button" class="btn btn-primary" type="button" onclick="onSearchClicked()">
<i class="fas fa-search fa-sm"></i>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack flex-wrap mb-5">
<div class="d-flex align-items-center position-relative my-2">
<div class="input-group">
<input name="search" id="idSearch" data-i18n="[placeholder]ip_list.search" type="text" class="form-control rounded-left w-250px" placeholder="Search" />
<button id="search_button" type="button" class="btn btn-primary">
<i class="ki-solid ki-magnifier fs-2"></i>
</button>
</div>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
<div>
<select id="idListType" name="list_type" class="form-select me-3" data-control="i18n-select2" data-hide-search="true">
<option data-i18n="ip_list.defender_list" value="2">Defender</option>
<option data-i18n="ip_list.allow_list" value="1">Allow list</option>
<option data-i18n="ip_list.ratelimiters_safe_list" value="3">Rate limiters safe list</option>
</select>
</div>
<a href="#" id="idAdd" class="btn btn-primary ms-5">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr>
<th>IP/Network</th>
<th>Protocols</th>
<th>Mode</th>
<th>Note</th>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th data-i18n="ip_list.ip_net">IP/Network</th>
<th data-i18n="ip_list.protocols">Protocols</th>
<th data-i18n="ip_list.mode">Mode</th>
<th data-i18n="general.description">Description</th>
<th class="min-w-100px"></th>
</tr>
</thead>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table>
</div>
<div id="paginationContainer" class="m-4 d-none">
<nav aria-label="Pagination">
<ul class="pagination justify-content-end">
<li id="pageItemPrev" class="page-item disabled"><a id="pagePrevious" class="page-link" href="#" onclick="prevClicked()">Previous</a></li>
<li id="pageItemNext" class="page-item disabled"><a id="pageNext" class="page-link" href="#" onclick="nextClicked()">Next</a></li>
</ul>
</nav>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to remove the selected entry?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
<div id="paginationContainer" class="d-flex mt-4 mb-4 justify-content-end d-none">
<div class="btn-group" role="group" aria-label="Pagination">
<button id="pagePrevious" data-i18n="general.previous" type="button" class="btn btn-outline btn-active-primary disabled">Previous</button>
<button id="pageNext" data-i18n="general.next" type="button" class="btn btn-outline btn-active-primary disabled">Next</button>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{- end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
{{- define "extra_js"}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
const prefListTypeName = 'sftpgo_pref_{{.LoggedAdmin.Username}}_iplist_type';
const prefListFilter = 'sftpgo_pref_{{.LoggedAdmin.Username}}_iplist_search_filter';
const listType = getListType();
const listFilter = getSearchFilter();
const prefListTypeName = 'sftpgo_pref_{{.LoggedUser.Username}}_iplist_type';
const prefListFilter = 'sftpgo_pref_{{.LoggedUser.Username}}_iplist_search_filter';
const listType = getListType();
const listFilter = getSearchFilter();
if (listType === '1' || listType === '3'){
$('#idListType').val(listType);
} else {
$('#idListType').val('2');
}
const pageSize = 15;
const paginationData = new Map();
if (listFilter){
$('#idIp').val(listFilter);
} else {
$('#idIp').val('');
}
const pageSize = 15;
const paginationData = new Map();
function saveListType(val) {
localStorage.setItem(prefListTypeName, val);
}
function getListType() {
return localStorage.getItem(prefListTypeName);
}
function saveSearchFilter() {
let val = $("#idIp").val();
if (val){
localStorage.setItem(prefListFilter, val);
} else {
localStorage.removeItem(prefListFilter);
function saveListType(val) {
localStorage.setItem(prefListTypeName, val);
}
}
function getSearchFilter() {
return localStorage.getItem(prefListFilter);
}
function getListType() {
return localStorage.getItem(prefListTypeName);
}
function resetPagination() {
$('#pageItemPrev').addClass("disabled");
$('#pageItemNext').addClass("disabled");
$('#paginationContainer').addClass("d-none");
paginationData.delete("firstIpOrNet");
paginationData.delete("lastIpOrNet");
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",false);
}
function prevClicked(){
paginationData.set("prevClicked",true);
paginationData.set("nextClicked",false);
doSearch();
}
function nextClicked(){
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",true);
doSearch();
}
function handleResponseData(data) {
let length = data.length;
let isNext = paginationData.get("nextClicked");
let isPrev = paginationData.get("prevClicked");
if (length > pageSize) {
data.pop();
length--;
if (isPrev || isNext){
$('#pageItemPrev').removeClass("disabled");
}
$('#pageItemNext').removeClass("disabled");
} else {
if (isPrev){
$('#pageItemPrev').addClass("disabled");
$('#pageItemNext').removeClass("disabled");
} else if (isNext){
$('#pageItemPrev').removeClass("disabled");
$('#pageItemNext').addClass("disabled");
function saveSearchFilter() {
let val = $("#idSearch").val();
if (val){
localStorage.setItem(prefListFilter, val);
} else {
$('#pageItemNext').addClass("disabled");
localStorage.removeItem(prefListFilter);
}
}
if (isPrev){
data = data.reverse();
}
if (length > 0){
paginationData.set("firstIpOrNet",data[0].ipornet);
paginationData.set("lastIpOrNet",data[length-1].ipornet);
$('#paginationContainer').removeClass("d-none");
} else {
resetPagination();
function getSearchFilter() {
return localStorage.getItem(prefListFilter);
}
return data;
}
function getSearchURL(){
let listType = fixedEncodeURIComponent($("#idListType").val());
let filter = encodeURIComponent($("#idIp").val());
let limit = pageSize + 1;
let from = "";
let order = "ASC"
if (paginationData.get("nextClicked") && paginationData.has("lastIpOrNet")){
from = encodeURIComponent(paginationData.get("lastIpOrNet"));
function resetPagination() {
$('#pagePrevious').addClass("disabled");
$('#pageNext').addClass("disabled");
$('#paginationContainer').addClass("d-none");
paginationData.delete("firstIpOrNet");
paginationData.delete("lastIpOrNet");
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",false);
}
if (paginationData.get("prevClicked") && paginationData.has("firstIpOrNet")){
from = encodeURIComponent(paginationData.get("firstIpOrNet"));
order = "DESC";
}
return "{{.IPListsURL}}"+`/${listType}?filter=${filter}&from=${from}&limit=${limit}&order=${order}`;
}
function deleteAction() {
let table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
let selectedRow = table.row({ selected: true }).data();
let path = '{{.IPListURL}}' + "/" + fixedEncodeURIComponent(selectedRow["type"])+"/"+ fixedEncodeURIComponent(selectedRow["ipornet"]);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
function handleResponseData(data) {
let length = data.length;
let isNext = paginationData.get("nextClicked");
let isPrev = paginationData.get("prevClicked");
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.IPListsURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
let txt = "Unable to delete the selected entry";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
if (length > pageSize) {
data.pop();
length--;
if (isPrev || isNext){
$('#pagePrevious').removeClass("disabled");
}
$('#pageNext').removeClass("disabled");
} else {
if (isPrev){
$('#pagePrevious').addClass("disabled");
$('#pageNext').removeClass("disabled");
} else if (isNext){
$('#pagePrevious').removeClass("disabled");
$('#pageNext').addClass("disabled");
} else {
$('#pageNext').addClass("disabled");
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
});
}
function setTableColumnVisibility(val){
let column = $('#dataTable').DataTable().column(2);
switch (val){
case '2':
column.visible(true);
break;
default:
column.visible(false);
}
}
function updateListTypeInfo(val) {
let info1 = $('#allowlist-info');
let info2 = $('#defender-info');
let info3 = $('#ratelimited-info');
if (info1){
info1.hide();
}
if (info2){
info2.hide();
}
if (info3){
info3.hide();
}
switch (val){
case '1':
if (info1){
info1.show();
}
break;
case '2':
if (info2){
info2.show();
}
break;
case '3':
if (info3){
info3.show();
}
break;
}
}
function onListChanged(val){
saveListType(val);
updateListTypeInfo(val);
setTableColumnVisibility(val);
let table = $('#dataTable').DataTable();
table.clear().draw();
table.ajax.url(getSearchURL()).load();
}
function onSearchClicked(){
resetPagination();
doSearch();
saveSearchFilter();
}
function doSearch(){
let table = $('#dataTable').DataTable();
table.clear().draw();
table.ajax.url(getSearchURL()).load();
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.IPListURL}}'+"/"+fixedEncodeURIComponent($("#idListType").val());
if (isPrev){
data = data.reverse();
}
if (length > 0){
paginationData.set("firstIpOrNet",data[0].ipornet);
paginationData.set("lastIpOrNet",data[length-1].ipornet);
$('#paginationContainer').removeClass("d-none");
} else {
resetPagination();
}
};
$.fn.dataTable.ext.buttons.edit = {
text: '<i class="fas fa-pen"></i>',
name: 'edit',
titleAttr: "Edit",
action: function (e, dt, node, config) {
let selectedRow = table.row({ selected: true }).data();
let path = '{{.IPListURL}}' + "/" + fixedEncodeURIComponent(selectedRow["type"])+"/"+ fixedEncodeURIComponent(selectedRow["ipornet"]);
window.location.href = path;
},
enabled: false
};
return data;
}
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
function getSearchURL(){
let listType = encodeURIComponent($("#idListType").val());
let filter = encodeURIComponent($("#idSearch").val());
let limit = pageSize + 1;
let from = "";
let order = "ASC"
if (paginationData.get("nextClicked") && paginationData.has("lastIpOrNet")){
from = encodeURIComponent(paginationData.get("lastIpOrNet"));
}
if (paginationData.get("prevClicked") && paginationData.has("firstIpOrNet")){
from = encodeURIComponent(paginationData.get("firstIpOrNet"));
order = "DESC";
}
return "{{.IPListsURL}}"+`/${listType}?filter=${filter}&from=${from}&limit=${limit}&order=${order}`;
}
let table = $('#dataTable').DataTable({
"ajax": {
"url": getSearchURL(),
"dataSrc": handleResponseData,
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "Failed to get IP list";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
function checkSelectedListType(val) {
switch (val){
case "1":
//{{- if not .IsAllowListEnabled}}
showToast(0, 'ip_list.allow_list_disabled');
//{{- end}}
break;
case "2":
//{{- if not .HasDefender}}
showToast(0, 'ip_list.defender_disabled');
//{{- end}}
break;
case "3":
//{{- if not .RateLimitersStatus}}
showToast(0, 'ip_list.ratelimiters_disabled');
//{{- end}}
break;
}
}
function deleteAction(listType, ipNet) {
ModalAlert.fire({
text: $.t('general.delete_confirm_generic'),
icon: "warning",
confirmButtonText: $.t('general.delete_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
},
"deferRender": true,
"processing": true,
"columns": [
{ "data": "ipornet" },
{
"data": "protocols",
"render": function (data, type, row) {
if (type === 'display') {
if (data == 0){
return "Any";
}
const protocols = [];
if ((data & 1) != 0){
protocols.push('SSH');
}
if ((data & 2) != 0){
protocols.push('FTP');
}
if ((data & 4) != 0){
protocols.push('DAV');
}
if ((data & 8) != 0){
protocols.push('HTTP');
}
return protocols.join(', ');
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.IPListURL}}' + "/" + encodeURIComponent(listType)+ "/" + encodeURIComponent(ipNet);
axios.delete(path, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
return data;
}
},
{
"data": "mode",
"render": function (data, type, row) {
if (type === 'display') {
if (data == 1){
return "Allow";
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
let errorMessage;
if (error && error.response) {
switch (error.response.status) {
case 403:
errorMessage = "general.delete_error_403";
break;
case 404:
errorMessage = "general.delete_error_404";
break;
}
return "Deny";
}
return data;
}
},
{
"data": "description",
"render": function (data, type, row) {
if (type === 'display') {
if (!data){
if (!errorMessage){
errorMessage = "general.delete_error_generic";
}
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
var datatable = function(){
var dt;
var initDatatable = function () {
$('#errorMsg').addClass("d-none");
dt = $('#dataTable').DataTable({
ajax: {
url: getSearchURL(),
dataSrc: handleResponseData,
error: function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
},
columns: [
{
data: "ipornet",
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "protocols",
render: function(data, type, row) {
if (type === 'display') {
if (data == 0){
return $.t('ip_list.any');
}
const protocols = [];
if ((data & 1) != 0){
protocols.push('SSH');
}
if ((data & 2) != 0){
protocols.push('FTP');
}
if ((data & 4) != 0){
protocols.push('DAV');
}
if ((data & 8) != 0){
protocols.push('HTTP');
}
return protocols.join(', ');
}
return data;
}
},
{
data: "mode",
render: function(data, type, row) {
if (type === 'display') {
if (data == 1){
return $.t('ip_list.allow');
}
return $.t('ip_list.deny');
}
return data;
}
},
{
data: "description",
visible: false,
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
return `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<span data-i18n="general.actions" class="fs-6">Actions</span>
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">
<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>
<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>
</div>`;
}
return "";
}
let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true, true);
return ellipsisFn(data,type);
}
return data;
},
],
deferRender: true,
processing: true,
lengthChange: false,
searching: false,
paging: false,
info: false,
ordering: false,
language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('datatable.no_records')
},
initComplete: function(settings, json) {
handleColumnVisibility($('#idListType').val());
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
let api = $.fn.dataTable.Api(settings);
api.columns.adjust().draw("page");
drawAction();
}
}
],
"select": {
"style": "single",
"blurable": true
},
"buttons": [],
"lengthChange": false,
"columnDefs": [],
"responsive": true,
"searching": false,
"paging": false,
"info": false,
"ordering": false,
"language": {
"loadingRecords": "",
"emptyTable": "No entries found"
},
"initComplete": function (settings, json) {
table.button().add(0, 'delete');
table.button().add(0, 'edit');
table.button().add(0, 'add');
});
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
dt.on('draw', drawAction);
}
function drawAction() {
KTMenu.createInstances();
handleRowActions();
$('#table_body').localize();
}
function handleColumnVisibility(val) {
switch (val){
case '2':
dt.column(2).visible(true);
break;
default:
dt.column(2).visible(false);
}
}
function doSearch(){
dt.clear().draw();
dt.ajax.url(getSearchURL()).load();
}
var handleDatatableActions = function () {
$('#idListType').on("change", function(e){
let val = $(this).find("option:selected").attr('value');
saveListType(val);
handleColumnVisibility(val);
doSearch();
checkSelectedListType(val);
});
$('#search_button').on("click", function(){
resetPagination();
doSearch();
saveSearchFilter();
});
$('#pagePrevious').on("click", function(e){
e.preventDefault();
this.blur();
paginationData.set("prevClicked",true);
paginationData.set("nextClicked",false);
doSearch();
});
$('#pageNext').on("click", function(e){
e.preventDefault();
this.blur();
paginationData.set("prevClicked",false);
paginationData.set("nextClicked",true);
doSearch();
});
}
function handleRowActions() {
const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
window.location.replace('{{.IPListURL}}' + "/" + encodeURIComponent(rowData['type'])+"/"+encodeURIComponent(rowData['ipornet']));
});
});
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
let rowData = dt.row(parent).data();
deleteAction(rowData['type'],rowData['ipornet']);
});
});
}
return {
init: function () {
initDatatable();
handleDatatableActions();
}
}
}();
$(document).on("i18nload", function(){
resetPagination();
if (listType === '1' || listType === '3'){
$('#idListType').val(listType);
} else {
$('#idListType').val('2');
}
$('#idListType').trigger('change');
if (listFilter){
$('#idSearch').val(listFilter);
} else {
$('#idSearch').val('');
}
$("#idAdd").on("click", function(){
window.location.href = '{{.IPListURL}}'+"/"+encodeURIComponent($("#idListType").val());
});
});
new $.fn.dataTable.FixedHeader(table);
$.fn.dataTable.ext.errMode = 'none';
table.on('select deselect', function () {
let selectedRows = table.rows({ selected: true }).count();
table.button('delete:name').enable(selectedRows == 1);
table.button('edit:name').enable(selectedRows == 1);
$(document).on("i18nshow", function(){
datatable.init();
checkSelectedListType(listType);
});
resetPagination();
let listType = $('#idListType').val();
setTableColumnVisibility(listType);
updateListTypeInfo(listType);
});
</script>
{{end}}
{{- end}}