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

@ -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 {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
function disconnectAction(connectionID, node) { function disconnectAction(connectionID, node) {
ModalAlert.fire({ ModalAlert.fire({
text: $.t('connections.disconnect_confirm'), text: $.t('connections.disconnect_confirm'),
icon: "warning", icon: "warning",

View file

@ -1,231 +1,284 @@
<!-- <!--
Copyright (C) 2019 Nicola Murino Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify 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>
<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>
</div> </div>
<div class="card-body"> <div id="card_body" class="card-body">
<div class="table-responsive"> <div id="loader" class="align-items-center text-center my-10">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <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> <thead>
<tr> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th>ID</th> <th data-i18n="defender.ip">IP</th>
<th>IP</th> <th data-i18n="defender.ban_time">Blocked until</th>
<th>Ban time</th> <th data-i18n="defender.scopre">Score</th>
<th>Score</th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{- end}}
{{define "dialog"}} {{- define "extra_js"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
aria-hidden="true"> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to remove the selected entry?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}} function deleteAction(id) {
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script> ModalAlert.fire({
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script> text: $.t('general.delete_confirm_generic'),
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script> icon: "warning",
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script> confirmButtonText: $.t('general.delete_confirm_btn'),
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script> cancelButtonText: $.t('general.cancel'),
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script> customClass: {
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script> confirmButton: "btn btn-danger",
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script> cancelButton: 'btn btn-secondary'
<script type="text/javascript"> }
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.DefenderHostsURL}}' + "/" + encodeURIComponent(id);
function deleteAction() { axios.delete(path, {
let table = $('#dataTable').DataTable(); timeout: 15000,
table.button('delete:name').enable(false); headers: {
let id = table.row({ selected: true }).data()["id"]; 'X-CSRF-TOKEN': '{{.CSRFToken}}'
let path = '{{.DefenderHostsURL}}' + "/" + fixedEncodeURIComponent(id); },
$('#deleteModal').modal('hide'); validateStatus: function (status) {
$('#errorMsg').hide(); return status == 200;
}
$.ajax({ }).then(function(response){
url: path, location.reload();
type: 'DELETE', }).catch(function(error){
dataType: 'json', KTApp.hidePageLoading();
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, let errorMessage;
timeout: 15000, if (error && error.response) {
success: function (result) { switch (error.response.status) {
window.location.href = '{{.DefenderURL}}'; case 403:
}, errorMessage = "general.delete_error_403";
error: function ($xhr, textStatus, errorThrown) { break;
let txt = "Unable to delete the selected entry"; case 404:
if ($xhr) { errorMessage = "general.delete_error_404";
let json = $xhr.responseJSON; break;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
} }
} }
} if (!errorMessage){
$('#errorTxt').text(txt); errorMessage = "general.delete_error_generic";
$('#errorMsg').show(); }
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
} }
}); });
} }
$(document).ready(function () { var datatable = function(){
$.fn.dataTable.ext.buttons.refresh = { var dt;
text: '<i class="fas fa-sync-alt"></i>',
name: 'refresh',
titleAttr: "Refresh",
action: function (e, dt, node, config) {
location.reload();
}
};
$.fn.dataTable.ext.buttons.delete = { var initDatatable = function () {
text: '<i class="fas fa-trash"></i>', $('#errorMsg').addClass("d-none");
name: 'delete', dt = $('#dataTable').DataTable({
titleAttr: "Delete", ajax: {
action: function (e, dt, node, config) { url: "{{.DefenderHostsURL}}",
$('#deleteModal').modal('show'); dataSrc: "",
}, error: function ($xhr, textStatus, errorThrown) {
enabled: false $(".dataTables_processing").hide();
}; let txt = "";
if ($xhr) {
let table = $('#dataTable').DataTable({ let json = $xhr.responseJSON;
"ajax": { if (json) {
"url": "{{.DefenderHostsURL}}", if (json.message){
"dataSrc": "", txt = json.message;
"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;
} }
} }
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": ""
}, },
{ columns: [
"data": "score", {
"defaultContent": "" data: "ip",
} defaultContent: "",
], render: function(data, type, row) {
"select": { if (type === 'display') {
"style": "single", return escapeHTML(data);
"blurable": true }
}, return data;
"buttons": [], }
"lengthChange": false, },
"columnDefs": [ {
{ data: "ban_time",
"targets": [0], searchable: false,
"visible": false, defaultContent: "",
"searchable": false 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')
}, },
], order: [[0, 'asc']],
"scrollX": false, initComplete: function(settings, json) {
"scrollY": false, $('#loader').addClass("d-none");
"responsive": true, $('#card_content').removeClass("d-none");
"language": { let api = $.fn.dataTable.Api(settings);
"loadingRecords": "", api.columns.adjust().draw("page");
"emptyTable": "No records found" drawAction();
}, }
"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']]
});
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();
}); }
{{end}}
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> </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>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <div class="d-flex justify-content-end mt-12">
<div class="col-sm-12 text-right px-0"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button> <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> </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,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 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">
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage IP Lists</h6>
</div> </div>
<div class="card-body"> <div id="card_body" class="card-body">
{{if not .HasDefender}} <div id="loader" class="align-items-center text-center my-10">
<div id="defender-info" class="card mb-3 border-left-info" style="display: none;"> <span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<div class="card-body">Defender disabled in your configuration</div> <span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div> </div>
{{end}} <div id="card_content" class="d-none">
{{if not .IsAllowListEnabled}} <div class="d-flex flex-stack flex-wrap mb-5">
<div id="allowlist-info" class="card mb-3 border-left-info" style="display: none;"> <div class="d-flex align-items-center position-relative my-2">
<div class="card-body">Allowlist disabled in your configuration</div> <div class="input-group">
</div> <input name="search" id="idSearch" data-i18n="[placeholder]ip_list.search" type="text" class="form-control rounded-left w-250px" placeholder="Search" />
{{end}} <button id="search_button" type="button" class="btn btn-primary">
{{if not .RateLimitersStatus}} <i class="ki-solid ki-magnifier fs-2"></i>
<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>
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
<div class="table-responsive"> <div class="d-flex justify-content-end my-2" data-table-toolbar="base">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <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> <thead>
<tr> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th>IP/Network</th> <th data-i18n="ip_list.ip_net">IP/Network</th>
<th>Protocols</th> <th data-i18n="ip_list.protocols">Protocols</th>
<th>Mode</th> <th data-i18n="ip_list.mode">Mode</th>
<th>Note</th> <th data-i18n="general.description">Description</th>
<th class="min-w-100px"></th>
</tr> </tr>
</thead> </thead>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table> </table>
</div>
<div id="paginationContainer" class="m-4 d-none"> <div id="paginationContainer" class="d-flex mt-4 mb-4 justify-content-end d-none">
<nav aria-label="Pagination"> <div class="btn-group" role="group" aria-label="Pagination">
<ul class="pagination justify-content-end"> <button id="pagePrevious" data-i18n="general.previous" type="button" class="btn btn-outline btn-active-primary disabled">Previous</button>
<li id="pageItemPrev" class="page-item disabled"><a id="pagePrevious" class="page-link" href="#" onclick="prevClicked()">Previous</a></li> <button id="pageNext" data-i18n="general.next" type="button" class="btn btn-outline btn-active-primary disabled">Next</button>
<li id="pageItemNext" class="page-item disabled"><a id="pageNext" class="page-link" href="#" onclick="nextClicked()">Next</a></li> </div>
</ul>
</nav>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to remove the selected entry?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{- 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 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'; const prefListTypeName = 'sftpgo_pref_{{.LoggedUser.Username}}_iplist_type';
const prefListFilter = 'sftpgo_pref_{{.LoggedAdmin.Username}}_iplist_search_filter'; 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'){ const pageSize = 15;
$('#idListType').val(listType); const paginationData = new Map();
} else {
$('#idListType').val('2');
}
if (listFilter){ function saveListType(val) {
$('#idIp').val(listFilter); localStorage.setItem(prefListTypeName, val);
} 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 getSearchFilter() { function getListType() {
return localStorage.getItem(prefListFilter); return localStorage.getItem(prefListTypeName);
} }
function resetPagination() { function saveSearchFilter() {
$('#pageItemPrev').addClass("disabled"); let val = $("#idSearch").val();
$('#pageItemNext').addClass("disabled"); if (val){
$('#paginationContainer').addClass("d-none"); localStorage.setItem(prefListFilter, val);
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");
} else { } else {
$('#pageItemNext').addClass("disabled"); localStorage.removeItem(prefListFilter);
} }
} }
if (isPrev){
data = data.reverse(); function getSearchFilter() {
} return localStorage.getItem(prefListFilter);
if (length > 0){
paginationData.set("firstIpOrNet",data[0].ipornet);
paginationData.set("lastIpOrNet",data[length-1].ipornet);
$('#paginationContainer').removeClass("d-none");
} else {
resetPagination();
} }
return data; function resetPagination() {
} $('#pagePrevious').addClass("disabled");
$('#pageNext').addClass("disabled");
function getSearchURL(){ $('#paginationContainer').addClass("d-none");
let listType = fixedEncodeURIComponent($("#idListType").val()); paginationData.delete("firstIpOrNet");
let filter = encodeURIComponent($("#idIp").val()); paginationData.delete("lastIpOrNet");
let limit = pageSize + 1; paginationData.set("prevClicked",false);
let from = ""; paginationData.set("nextClicked",false);
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}`;
}
function deleteAction() { function handleResponseData(data) {
let table = $('#dataTable').DataTable(); let length = data.length;
table.button('delete:name').enable(false); let isNext = paginationData.get("nextClicked");
let selectedRow = table.row({ selected: true }).data(); let isPrev = paginationData.get("prevClicked");
let path = '{{.IPListURL}}' + "/" + fixedEncodeURIComponent(selectedRow["type"])+"/"+ fixedEncodeURIComponent(selectedRow["ipornet"]);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
$.ajax({ if (length > pageSize) {
url: path, data.pop();
type: 'DELETE', length--;
dataType: 'json', if (isPrev || isNext){
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, $('#pagePrevious').removeClass("disabled");
timeout: 15000, }
success: function (result) { $('#pageNext').removeClass("disabled");
window.location.href = '{{.IPListsURL}}'; } else {
}, if (isPrev){
error: function ($xhr, textStatus, errorThrown) { $('#pagePrevious').addClass("disabled");
let txt = "Unable to delete the selected entry"; $('#pageNext').removeClass("disabled");
if ($xhr) { } else if (isNext){
let json = $xhr.responseJSON; $('#pagePrevious').removeClass("disabled");
if (json) { $('#pageNext').addClass("disabled");
if (json.message){ } else {
txt += ": " + json.message; $('#pageNext').addClass("disabled");
} else {
txt += ": " + json.error;
}
}
} }
$('#errorTxt').text(txt);
$('#errorMsg').show();
} }
}); if (isPrev){
} data = data.reverse();
}
function setTableColumnVisibility(val){ if (length > 0){
let column = $('#dataTable').DataTable().column(2); paginationData.set("firstIpOrNet",data[0].ipornet);
paginationData.set("lastIpOrNet",data[length-1].ipornet);
switch (val){ $('#paginationContainer').removeClass("d-none");
case '2': } else {
column.visible(true); resetPagination();
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());
} }
};
$.fn.dataTable.ext.buttons.edit = { return data;
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 = { function getSearchURL(){
text: '<i class="fas fa-trash"></i>', let listType = encodeURIComponent($("#idListType").val());
name: 'delete', let filter = encodeURIComponent($("#idSearch").val());
titleAttr: "Delete", let limit = pageSize + 1;
action: function (e, dt, node, config) { let from = "";
$('#deleteModal').modal('show'); let order = "ASC"
}, if (paginationData.get("nextClicked") && paginationData.has("lastIpOrNet")){
enabled: false 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({ function checkSelectedListType(val) {
"ajax": { switch (val){
"url": getSearchURL(), case "1":
"dataSrc": handleResponseData, //{{- if not .IsAllowListEnabled}}
"error": function ($xhr, textStatus, errorThrown) { showToast(0, 'ip_list.allow_list_disabled');
$(".dataTables_processing").hide(); //{{- end}}
let txt = "Failed to get IP list"; break;
if ($xhr) { case "2":
let json = $xhr.responseJSON; //{{- if not .HasDefender}}
if (json) { showToast(0, 'ip_list.defender_disabled');
if (json.message){ //{{- end}}
txt += ": " + json.message; break;
} else { case "3":
txt += ": " + json.error; //{{- if not .RateLimitersStatus}}
} showToast(0, 'ip_list.ratelimiters_disabled');
} //{{- end}}
} break;
$('#errorTxt').text(txt); }
$('#errorMsg').show(); }
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'
} }
}, }).then((result) => {
"deferRender": true, if (result.isConfirmed){
"processing": true, $('#loading_message').text("");
"columns": [ KTApp.showPageLoading();
{ "data": "ipornet" }, let path = '{{.IPListURL}}' + "/" + encodeURIComponent(listType)+ "/" + encodeURIComponent(ipNet);
{ axios.delete(path, {
"data": "protocols", timeout: 15000,
"render": function (data, type, row) { headers: {
if (type === 'display') { 'X-CSRF-TOKEN': '{{.CSRFToken}}'
if (data == 0){ },
return "Any"; validateStatus: function (status) {
} return status == 200;
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; }).then(function(response){
} location.reload();
}, }).catch(function(error){
{ KTApp.hidePageLoading();
"data": "mode", let errorMessage;
"render": function (data, type, row) { if (error && error.response) {
if (type === 'display') { switch (error.response.status) {
if (data == 1){ case 403:
return "Allow"; errorMessage = "general.delete_error_403";
break;
case 404:
errorMessage = "general.delete_error_404";
break;
} }
return "Deny";
} }
return data; if (!errorMessage){
} errorMessage = "general.delete_error_generic";
}, }
{ ModalAlert.fire({
"data": "description", text: $.t(errorMessage),
"render": function (data, type, row) { icon: "warning",
if (type === 'display') { confirmButtonText: $.t('general.ok'),
if (!data){ 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 ""; return "";
} }
let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true, true); },
return ellipsisFn(data,type); ],
} deferRender: true,
return data; 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); $(document).on("i18nshow", function(){
$.fn.dataTable.ext.errMode = 'none'; datatable.init();
checkSelectedListType(listType);
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);
}); });
resetPagination();
let listType = $('#idListType').val();
setTableColumnVisibility(listType);
updateListTypeInfo(listType);
});
</script> </script>
{{end}} {{- end}}