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/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 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 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/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/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/marketplacemetering v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 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/go-sql-driver/mysql v1.7.1
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 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-hclog v1.6.2
github.com/hashicorp/go-plugin v1.6.0 github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.5 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/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/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/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/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/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // 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/tools v0.17.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.60.1 // indirect google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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 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 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/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.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
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/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 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/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 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/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.14 h1:ogP1WgyvN/qxPJkgtFMD7G2eKb5p/61Jomx+nIHXUQ4=
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/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 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/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 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/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.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
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/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 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/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= 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/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/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-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY=
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/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 h1:Wlv9TzkrG9V7i6u8dEtmXPrBzvfFp+CgJNs696rAajM=
github.com/cockroachdb/cockroach-go/v2 v2.3.6/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= 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= 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/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.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.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= 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-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-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-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-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s= google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= 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-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 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.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= 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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 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 return err
} }
if e := bucket.Get([]byte(entry.getKey())); e != nil { 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.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
entry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) entry.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())

View file

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

View file

@ -214,7 +214,7 @@ func (e *IPListEntry) validate() error {
// parse as IP // parse as IP
parsed, err := netip.ParseAddr(e.IPOrNet) parsed, err := netip.ParseAddr(e.IPOrNet)
if err != nil { 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() { if parsed.Is4() {
e.IPOrNet += "/32" e.IPOrNet += "/32"
@ -226,7 +226,7 @@ func (e *IPListEntry) validate() error {
} }
prefix, err := netip.ParsePrefix(e.IPOrNet) prefix, err := netip.ParsePrefix(e.IPOrNet)
if err != nil { 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() prefix = prefix.Masked()
if prefix.Addr().Is4In6() { if prefix.Addr().Is4In6() {
@ -235,7 +235,7 @@ func (e *IPListEntry) validate() error {
// TODO: to remove when the in memory ranger switch to netip // TODO: to remove when the in memory ranger switch to netip
_, _, err = net.ParseCIDR(e.IPOrNet) _, _, err = net.ParseCIDR(e.IPOrNet)
if err != nil { 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() { if prefix.Addr().Is4() || prefix.Addr().Is4In6() {
e.IPType = ipTypeV4 e.IPType = ipTypeV4

View file

@ -2672,7 +2672,10 @@ func (p *MemoryProvider) addIPListEntry(entry *IPListEntry) error {
} }
_, err := p.ipListEntryExistsInternal(entry) _, err := p.ipListEntryExistsInternal(entry)
if err == nil { 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.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
entry.UpdatedAt = 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 { 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 { 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) { if errors.As(err, &mysqlErr) {
switch mysqlErr.Number { switch mysqlErr.Number {
case 1062: case 1062:
message := util.I18nErrorDuplicatedName var message string
if fieldType == fieldUsername { switch fieldType {
case fieldUsername:
message = util.I18nErrorDuplicatedUsername message = util.I18nErrorDuplicatedUsername
case fieldIPNet:
message = util.I18nErrorDuplicatedIPNet
default:
message = util.I18nErrorDuplicatedName
} }
return util.NewI18nError( return util.NewI18nError(
fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()), 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 { 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 { 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) { if errors.As(err, &pgsqlErr) {
switch pgsqlErr.Code { switch pgsqlErr.Code {
case "23505": case "23505":
message := util.I18nErrorDuplicatedName var message string
if fieldType == fieldUsername { switch fieldType {
case fieldUsername:
message = util.I18nErrorDuplicatedUsername message = util.I18nErrorDuplicatedUsername
case fieldIPNet:
message = util.I18nErrorDuplicatedIPNet
default:
message = util.I18nErrorDuplicatedName
} }
return util.NewI18nError( return util.NewI18nError(
fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()), 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 { 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 { 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 { if e, ok := err.(sqlite3.Error); ok {
switch e.ExtendedCode { switch e.ExtendedCode {
case 1555, 2067: case 1555, 2067:
message := util.I18nErrorDuplicatedName var message string
if fieldType == fieldUsername { switch fieldType {
case fieldUsername:
message = util.I18nErrorDuplicatedUsername message = util.I18nErrorDuplicatedUsername
case fieldIPNet:
message = util.I18nErrorDuplicatedIPNet
default:
message = util.I18nErrorDuplicatedName
} }
return util.NewI18nError( return util.NewI18nError(
fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()), fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),

View file

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

View file

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

View file

@ -58,7 +58,9 @@
"add_role": "Add role", "add_role": "Add role",
"update_role": "Update role", "update_role": "Update role",
"add_admin": "Add admin", "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": { "setup": {
"desc": "To start using SFTPGo you need to create an administrator user", "desc": "To start using SFTPGo you need to create an administrator user",
@ -229,7 +231,10 @@
"members": "Members", "members": "Members",
"members_summary": "Users: {{users}}. Admins: {{admins}}", "members_summary": "Users: {{users}}. Admins: {{admins}}",
"status": "Status", "status": "Status",
"last_login": "Last login" "last_login": "Last login",
"previous": "Previous",
"next": "Next",
"type": "Type"
}, },
"fs": { "fs": {
"view_file": "View file \"{{- path}}\"", "view_file": "View file \"{{- path}}\"",
@ -710,5 +715,31 @@
}, },
"role": { "role": {
"view_manage": "View and manage roles" "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", "add_role": "Aggiungi ruolo",
"update_role": "Aggiorna ruolo", "update_role": "Aggiorna ruolo",
"add_admin": "Aggiungi amministratore", "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": { "setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore", "desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
@ -229,7 +231,10 @@
"members": "Membri", "members": "Membri",
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}", "members_summary": "Utenti: {{users}}. Amministratori: {{admins}}",
"status": "Stato", "status": "Stato",
"last_login": "Ultimo accesso" "last_login": "Ultimo accesso",
"previous": "Precedente",
"next": "Successivo",
"type": "Tipo"
}, },
"fs": { "fs": {
"view_file": "Visualizza file \"{{- path}}\"", "view_file": "Visualizza file \"{{- path}}\"",
@ -710,5 +715,31 @@
}, },
"role": { "role": {
"view_manage": "Visualizza e gestisci ruoli" "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

@ -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 This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. 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" .}} {{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"}} {{- define "page_body"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet"> {{- template "errmsg" ""}}
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet"> <div class="card shadow-sm">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet"> <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet"> <h3 data-i18n="defender.view_manage" class="card-title section-title">View and manage auto blocklis</h3>
<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> </div>
<script type="text/javascript"> <div id="card_body" class="card-body">
function dismissErrorMsg(){ <div id="loader" class="align-items-center text-center my-10">
$('#errorMsg').hide(); <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>
</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>
</div> </div>
<div class="card-body"> <div id="card_content" class="d-none">
<div class="table-responsive"> <div class="d-flex flex-stack flex-wrap mb-5">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <div class="d-flex align-items-center position-relative my-2">
<thead> <i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
<tr> <input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
<th>ID</th> class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
<th>IP</th>
<th>Ban time</th>
<th>Score</th>
</tr>
</thead>
</table>
</div> </div>
</div> <div class="d-flex justify-content-end my-2" data-table-toolbar="base">
</div> <a href="{{.DefenderURL}}" class="btn btn-primary">
{{end}} <i class="ki-solid ki-arrows-circle fs-2"></i>
<span data-i18n="general.refresh">Refresh</span>
{{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> </a>
</div> </div>
</div> </div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<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> </div>
{{end}} </div>
{{- end}}
{{define "extra_js"}} {{- define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
<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() { function deleteAction(id) {
let table = $('#dataTable').DataTable(); ModalAlert.fire({
table.button('delete:name').enable(false); text: $.t('general.delete_confirm_generic'),
let id = table.row({ selected: true }).data()["id"]; icon: "warning",
let path = '{{.DefenderHostsURL}}' + "/" + fixedEncodeURIComponent(id); confirmButtonText: $.t('general.delete_confirm_btn'),
$('#deleteModal').modal('hide'); cancelButtonText: $.t('general.cancel'),
$('#errorMsg').hide(); customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.DefenderHostsURL}}' + "/" + encodeURIComponent(id);
$.ajax({ axios.delete(path, {
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000, timeout: 15000,
success: function (result) { headers: {
window.location.href = '{{.DefenderURL}}'; 'X-CSRF-TOKEN': '{{.CSRFToken}}'
}, },
error: function ($xhr, textStatus, errorThrown) { validateStatus: function (status) {
let txt = "Unable to delete the selected entry"; return status == 200;
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
} }
} }).then(function(response){
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
});
}
$(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(); 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;
}
}
if (!errorMessage){
errorMessage = "general.delete_error_generic";
}
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
} }
};
$.fn.dataTable.ext.buttons.delete = { var datatable = function(){
text: '<i class="fas fa-trash"></i>', var dt;
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
let table = $('#dataTable').DataTable({ var initDatatable = function () {
"ajax": { $('#errorMsg').addClass("d-none");
"url": "{{.DefenderHostsURL}}", dt = $('#dataTable').DataTable({
"dataSrc": "", ajax: {
"error": function ($xhr, textStatus, errorThrown) { url: "{{.DefenderHostsURL}}",
dataSrc: "",
error: function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide(); $(".dataTables_processing").hide();
let txt = "Failed to get auto blocklist"; let txt = "";
if ($xhr) { if ($xhr) {
let json = $xhr.responseJSON; let json = $xhr.responseJSON;
if (json) { if (json) {
if (json.message){ if (json.message){
txt += ": " + json.message; txt = json.message;
} else {
txt += ": " + json.error;
} }
} }
} }
$('#errorTxt').text(txt); if (!txt){
$('#errorMsg').show(); txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
} }
}, },
"deferRender": true, columns: [
"processing": true,
"columns": [
{ "data": "id" },
{ "data": "ip" },
{ {
"data": "ban_time", data: "ip",
"defaultContent": "" defaultContent: "",
}, render: function(data, type, row) {
{ if (type === 'display') {
"data": "score", return escapeHTML(data);
"defaultContent": "" }
return data;
} }
],
"select": {
"style": "single",
"blurable": true
}, },
"buttons": [],
"lengthChange": false,
"columnDefs": [
{ {
"targets": [0], data: "ban_time",
"visible": false, searchable: false,
"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 "";
}
}, },
], ],
"scrollX": false, deferRender: true,
"scrollY": false, stateSave: true,
"responsive": true, stateDuration: 0,
"language": { stateLoadParams: function (settings, data) {
"loadingRecords": "", if (data.search.search){
"emptyTable": "No records found" const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
}
}, },
"initComplete": function (settings, json) { language: {
{{if .LoggedAdmin.HasPermission "manage_defender"}} info: $.t('datatable.info'),
table.button().add(0, 'delete'); infoEmpty: $.t('datatable.info_empty'),
{{end}} infoFiltered: $.t('datatable.info_filtered'),
table.button().add(0, 'pageLength'); loadingRecords: "",
table.button().add(0, 'refresh'); processing: $.t('datatable.processing'),
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container()); zeroRecords: "",
emptyTable: $.t('datatable.no_records')
}, },
"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); dt.on('draw', drawAction);
$.fn.dataTable.ext.errMode = 'none'; }
{{if .LoggedAdmin.HasPermission "manage_defender"}} function drawAction() {
table.on('select deselect', function () { KTMenu.createInstances();
let selectedRows = table.rows({ selected: true }).count(); handleRowActions();
table.button('delete:name').enable(selectedRows == 1); $('#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();
}); });
{{end}} }
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> </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 This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. 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" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}} {{- define "page_body"}}
<div class="card shadow-sm">
{{define "extra_css"}} <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet"> <h3 data-i18n="{{.Title}}" class="card-title section-title"></h3>
{{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>
</div> </div>
<div class="card-body"> <div class="card-body">
{{if .Error}} {{- template "errmsg" .Error}}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
{{.Error}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{end}}
<form id="iplist_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"> <form id="iplist_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row"> <div class="form-group row">
<label for="idIPOrNet" class="col-sm-2 col-form-label">IP/Network</label> <label for="idType" data-i18n="general.type" class="col-md-3 col-form-label">Type</label>
<div class="col-sm-10"> <div class="col-md-9">
<input type="text" class="form-control" id="idIPOrNet" name="ipornet" placeholder="" <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"
value="{{.Entry.IPOrNet}}" maxlength="50" autocomplete="nope" aria-describedby="ipOrNetHelpBlock" required {{if eq .Mode 2}}readonly{{end}}> class="form-control-plaintext readonly-input" readonly />
{{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}}
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row mt-10">
<label for="idType" class="col-sm-2 col-form-label">Type</label> <label for="idIPOrNet" data-i18n="ip_list.ip_net" class="col-md-3 col-form-label">IP/Network</label>
<div class="col-sm-10"> <div class="col-md-9">
<input type="text" class="form-control" id="idType" name="type" placeholder="" <input id="idIPOrNet" type="text" name="ipornet" value="{{.Entry.IPOrNet}}" maxlength="50" autocomplete="off"
value="{{.Entry.Type.AsString}}" maxlength="50" readonly> 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>
</div> </div>
{{if eq .Entry.Type 2}} {{- if eq .Entry.Type 2}}
<div class="form-group row"> <div class="form-group row mt-10">
<label for="idMode" class="col-sm-2 col-form-label">Mode</label> <label for="idMode" data-i18n="ip_list.mode" class="col-md-3 col-form-label">Mode</label>
<div class="col-sm-10"> <div class="col-md-9">
<select class="form-control selectpicker" id="idMode" name="mode"> <select id="idMode" name="mode" class="form-select" data-control="i18n-select2" data-hide-search="true">
<option value="2" {{if eq .Entry.Mode 2 }}selected{{end}}>Deny</option> <option value="2" data-i18n="ip_list.deny" {{if eq .Entry.Mode 2 }}selected{{end}}>Deny</option>
<option value="1" {{if eq .Entry.Mode 1 }}selected{{end}}>Allow</option> <option value="1" data-i18n="ip_list.allow" {{if eq .Entry.Mode 1 }}selected{{end}}>Allow</option>
</select> </select>
</div> </div>
</div> </div>
{{end}} {{- end}}
<div class="form-group row"> <div class="form-group row mt-10">
<label for="idProtocols" class="col-sm-2 col-form-label">Protocols</label> <label for="idProtocols" data-i18n="ip_list.protocols" class="col-md-3 col-form-label">
<div class="col-sm-10"> Protocols
<select class="form-control selectpicker" id="idProtocols" name="protocols" multiple title="Any"> </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="1" {{if .Entry.HasProtocol "SSH" }}selected{{end}}>SSH</option>
<option value="2" {{if .Entry.HasProtocol "FTP" }}selected{{end}}>FTP</option> <option value="2" {{if .Entry.HasProtocol "FTP" }}selected{{end}}>FTP</option>
<option value="4" {{if .Entry.HasProtocol "DAV" }}selected{{end}}>DAV</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> </div>
<div class="form-group row"> <div class="form-group row mt-10">
<label for="idDescription" class="col-sm-2 col-form-label">Note</label> <label for="idDescription" data-i18n="general.description" class="col-md-3 col-form-label">Description</label>
<div class="col-sm-10"> <div class="col-md-9">
<input type="text" class="form-control" id="idDescription" name="description" placeholder="" <input id="idDescription" type="text" class="form-control" name="description" value="{{.Entry.Description}}" maxlength="512">
value="{{.Entry.Description}}" maxlength="512" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional note
</small>
</div> </div>
</div> </div>
<div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<div class="col-sm-12 text-right px-0"> <button type="submit" id="form_submit" class="btn btn-primary px-10" name="form_action" value="submit">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button> <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> </div>
</form> </form>
</div> </div>
</div> </div>
{{end}} {{- end}}
{{- define "extra_js"}}
{{define "extra_js"}} <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script> $(document).on("i18nshow", function(){
{{end}} $('#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,168 +1,96 @@
<!-- <!--
Copyright (C) 2019 Nicola Murino Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. 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" .}} {{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"}} {{- define "page_body"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet"> {{- template "errmsg" ""}}
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet"> <div class="card shadow-sm">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet"> <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet"> <h3 data-i18n="ip_list.view_manage" class="card-title section-title">View and manage IP Lists</h3>
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet"> </div>
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet"> <div id="card_body" class="card-body">
{{end}} <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>
{{define "page_body"}} <span data-i18n="general.loading" class="text-gray-700">Loading...</span>
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert"> </div>
<span id="errorTxt"></span> <div id="card_content" class="d-none">
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();"> <div class="d-flex flex-stack flex-wrap mb-5">
<span aria-hidden="true">&times;</span> <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> </button>
</div> </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>
</div> </div>
<div class="card-body">
{{if not .HasDefender}} <div class="d-flex justify-content-end my-2" data-table-toolbar="base">
<div id="defender-info" class="card mb-3 border-left-info" style="display: none;"> <div>
<div class="card-body">Defender disabled in your configuration</div> <select id="idListType" name="list_type" class="form-select me-3" data-control="i18n-select2" data-hide-search="true">
</div> <option data-i18n="ip_list.defender_list" value="2">Defender</option>
{{end}} <option data-i18n="ip_list.allow_list" value="1">Allow list</option>
{{if not .IsAllowListEnabled}} <option data-i18n="ip_list.ratelimiters_safe_list" value="3">Rate limiters safe list</option>
<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> </select>
</div> </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>
</button>
</div>
</div>
</div>
</div>
<div class="table-responsive"> <a href="#" id="idAdd" class="btn btn-primary ms-5">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <i class="ki-duotone ki-plus fs-2"></i>
<thead> <span data-i18n="general.add">Add</span>
<tr>
<th>IP/Network</th>
<th>Protocols</th>
<th>Mode</th>
<th>Note</th>
</tr>
</thead>
</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> </a>
</div> </div>
</div> </div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<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 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>
{{end}}
{{define "extra_js"}} </div>
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script> </div>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script> </div>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script> {{- end}}
<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">
const prefListTypeName = 'sftpgo_pref_{{.LoggedAdmin.Username}}_iplist_type'; {{- define "extra_js"}}
const prefListFilter = 'sftpgo_pref_{{.LoggedAdmin.Username}}_iplist_search_filter'; <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_{{.LoggedUser.Username}}_iplist_type';
const prefListFilter = 'sftpgo_pref_{{.LoggedUser.Username}}_iplist_search_filter';
const listType = getListType(); const listType = getListType();
const listFilter = getSearchFilter(); const listFilter = getSearchFilter();
if (listType === '1' || listType === '3'){
$('#idListType').val(listType);
} else {
$('#idListType').val('2');
}
if (listFilter){
$('#idIp').val(listFilter);
} else {
$('#idIp').val('');
}
const pageSize = 15; const pageSize = 15;
const paginationData = new Map(); const paginationData = new Map();
@ -175,7 +103,7 @@ function getListType() {
} }
function saveSearchFilter() { function saveSearchFilter() {
let val = $("#idIp").val(); let val = $("#idSearch").val();
if (val){ if (val){
localStorage.setItem(prefListFilter, val); localStorage.setItem(prefListFilter, val);
} else { } else {
@ -188,8 +116,8 @@ function getSearchFilter() {
} }
function resetPagination() { function resetPagination() {
$('#pageItemPrev').addClass("disabled"); $('#pagePrevious').addClass("disabled");
$('#pageItemNext').addClass("disabled"); $('#pageNext').addClass("disabled");
$('#paginationContainer').addClass("d-none"); $('#paginationContainer').addClass("d-none");
paginationData.delete("firstIpOrNet"); paginationData.delete("firstIpOrNet");
paginationData.delete("lastIpOrNet"); paginationData.delete("lastIpOrNet");
@ -197,18 +125,6 @@ function resetPagination() {
paginationData.set("nextClicked",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) { function handleResponseData(data) {
let length = data.length; let length = data.length;
let isNext = paginationData.get("nextClicked"); let isNext = paginationData.get("nextClicked");
@ -218,18 +134,18 @@ function handleResponseData(data) {
data.pop(); data.pop();
length--; length--;
if (isPrev || isNext){ if (isPrev || isNext){
$('#pageItemPrev').removeClass("disabled"); $('#pagePrevious').removeClass("disabled");
} }
$('#pageItemNext').removeClass("disabled"); $('#pageNext').removeClass("disabled");
} else { } else {
if (isPrev){ if (isPrev){
$('#pageItemPrev').addClass("disabled"); $('#pagePrevious').addClass("disabled");
$('#pageItemNext').removeClass("disabled"); $('#pageNext').removeClass("disabled");
} else if (isNext){ } else if (isNext){
$('#pageItemPrev').removeClass("disabled"); $('#pagePrevious').removeClass("disabled");
$('#pageItemNext').addClass("disabled"); $('#pageNext').addClass("disabled");
} else { } else {
$('#pageItemNext').addClass("disabled"); $('#pageNext').addClass("disabled");
} }
} }
if (isPrev){ if (isPrev){
@ -247,8 +163,8 @@ function handleResponseData(data) {
} }
function getSearchURL(){ function getSearchURL(){
let listType = fixedEncodeURIComponent($("#idListType").val()); let listType = encodeURIComponent($("#idListType").val());
let filter = encodeURIComponent($("#idIp").val()); let filter = encodeURIComponent($("#idSearch").val());
let limit = pageSize + 1; let limit = pageSize + 1;
let from = ""; let from = "";
let order = "ASC" let order = "ASC"
@ -262,169 +178,123 @@ function getSearchURL(){
return "{{.IPListsURL}}"+`/${listType}?filter=${filter}&from=${from}&limit=${limit}&order=${order}`; return "{{.IPListsURL}}"+`/${listType}?filter=${filter}&from=${from}&limit=${limit}&order=${order}`;
} }
function deleteAction() { function checkSelectedListType(val) {
let table = $('#dataTable').DataTable(); switch (val){
table.button('delete:name').enable(false); case "1":
let selectedRow = table.row({ selected: true }).data(); //{{- if not .IsAllowListEnabled}}
let path = '{{.IPListURL}}' + "/" + fixedEncodeURIComponent(selectedRow["type"])+"/"+ fixedEncodeURIComponent(selectedRow["ipornet"]); showToast(0, 'ip_list.allow_list_disabled');
$('#deleteModal').modal('hide'); //{{- end}}
$('#errorMsg').hide(); 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;
}
}
$.ajax({ function deleteAction(listType, ipNet) {
url: path, ModalAlert.fire({
type: 'DELETE', text: $.t('general.delete_confirm_generic'),
dataType: 'json', icon: "warning",
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, 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 = '{{.IPListURL}}' + "/" + encodeURIComponent(listType)+ "/" + encodeURIComponent(ipNet);
axios.delete(path, {
timeout: 15000, timeout: 15000,
success: function (result) { headers: {
window.location.href = '{{.IPListsURL}}'; 'X-CSRF-TOKEN': '{{.CSRFToken}}'
}, },
error: function ($xhr, textStatus, errorThrown) { validateStatus: function (status) {
let txt = "Unable to delete the selected entry"; return status == 200;
if ($xhr) { }
let json = $xhr.responseJSON; }).then(function(response){
if (json) { location.reload();
if (json.message){ }).catch(function(error){
txt += ": " + json.message; KTApp.hidePageLoading();
} else { let errorMessage;
txt += ": " + json.error; if (error && error.response) {
switch (error.response.status) {
case 403:
errorMessage = "general.delete_error_403";
break;
case 404:
errorMessage = "general.delete_error_404";
break;
} }
} }
if (!errorMessage){
errorMessage = "general.delete_error_generic";
} }
$('#errorTxt').text(txt); ModalAlert.fire({
$('#errorMsg').show(); text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
} }
}); });
} }
function setTableColumnVisibility(val){ var datatable = function(){
let column = $('#dataTable').DataTable().column(2); var dt;
switch (val){ var initDatatable = function () {
case '2': $('#errorMsg').addClass("d-none");
column.visible(true); dt = $('#dataTable').DataTable({
break; ajax: {
default: url: getSearchURL(),
column.visible(false); dataSrc: handleResponseData,
} error: function ($xhr, textStatus, errorThrown) {
}
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());
}
};
$.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
};
$.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": getSearchURL(),
"dataSrc": handleResponseData,
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide(); $(".dataTables_processing").hide();
let txt = "Failed to get IP list"; let txt = "";
if ($xhr) { if ($xhr) {
let json = $xhr.responseJSON; let json = $xhr.responseJSON;
if (json) { if (json) {
if (json.message){ if (json.message){
txt += ": " + json.message; txt = json.message;
} else {
txt += ": " + json.error;
} }
} }
} }
$('#errorTxt').text(txt); if (!txt){
$('#errorMsg').show(); txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
} }
}, },
"deferRender": true, columns: [
"processing": true,
"columns": [
{ "data": "ipornet" },
{ {
"data": "protocols", data: "ipornet",
"render": function (data, type, row) { render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "protocols",
render: function(data, type, row) {
if (type === 'display') { if (type === 'display') {
if (data == 0){ if (data == 0){
return "Any"; return $.t('ip_list.any');
} }
const protocols = []; const protocols = [];
if ((data & 1) != 0){ if ((data & 1) != 0){
@ -445,71 +315,187 @@ $(document).ready(function () {
} }
}, },
{ {
"data": "mode", data: "mode",
"render": function (data, type, row) { render: function(data, type, row) {
if (type === 'display') { if (type === 'display') {
if (data == 1){ if (data == 1){
return "Allow"; return $.t('ip_list.allow');
} }
return "Deny"; return $.t('ip_list.deny');
} }
return data; return data;
} }
}, },
{ {
"data": "description", data: "description",
"render": function (data, type, row) { visible: false,
render: function(data, type, row) {
if (type === 'display') { if (type === 'display') {
if (!data){ return escapeHTML(data);
return "";
}
let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true, true);
return ellipsisFn(data,type);
} }
return 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 "";
}
},
], ],
"select": { deferRender: true,
"style": "single", processing: true,
"blurable": 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')
}, },
"buttons": [], initComplete: function(settings, json) {
"lengthChange": false, handleColumnVisibility($('#idListType').val());
"columnDefs": [], $('#loader').addClass("d-none");
"responsive": true, $('#card_content').removeClass("d-none");
"searching": false, let api = $.fn.dataTable.Api(settings);
"paging": false, api.columns.adjust().draw("page");
"info": false, drawAction();
"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());
} }
}); });
new $.fn.dataTable.FixedHeader(table); dt.on('draw', drawAction);
$.fn.dataTable.ext.errMode = 'none'; }
table.on('select deselect', function () { function drawAction() {
let selectedRows = table.rows({ selected: true }).count(); KTMenu.createInstances();
table.button('delete:name').enable(selectedRows == 1); handleRowActions();
table.button('edit:name').enable(selectedRows == 1); $('#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(); resetPagination();
doSearch();
let listType = $('#idListType').val(); saveSearchFilter();
setTableColumnVisibility(listType);
updateListTypeInfo(listType);
}); });
$('#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());
});
});
$(document).on("i18nshow", function(){
datatable.init();
checkSelectedListType(listType);
});
</script> </script>
{{end}} {{- end}}