mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-24 16:40:26 +00:00
WIP new WebAdmin: IP lists pages
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
d381304136
commit
8180b75ef1
17 changed files with 915 additions and 789 deletions
16
go.mod
16
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -151,6 +151,7 @@ const (
|
|||
const (
|
||||
fieldUsername = 1
|
||||
fieldName = 2
|
||||
fieldIPNet = 3
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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">×</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">×</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}}
|
|
@ -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">×</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}}
|
|
@ -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">×</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">×</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}}
|
Loading…
Reference in a new issue