wip: 完成 client api 分析
This commit is contained in:
commit
64cdd0cb0e
.gitignore.nfctlDockerfilemain.goreadme.md
deployment
etc
go.modgo.sumhttptest
internal
api
cmd
controller
database
cache
db
es
gateway
handler
interfaces
invoke
log
middleware
model
opt
sqlType
tool
unix
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
.data
|
||||
dist
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
xtest
|
||||
*.sock
|
||||
__debug*
|
35
.nfctl
Normal file
35
.nfctl
Normal file
@ -0,0 +1,35 @@
|
||||
# nfctl init script
|
||||
# https://github.com/loveuer/nf/nft/nfctl
|
||||
or
|
||||
# https://gitcode.com/loveuer/nf/nft/nfctl
|
||||
|
||||
# 替换 import
|
||||
!replace content
|
||||
suffix .go
|
||||
ultone => {{.PROJECT_NAME}}
|
||||
EOF
|
||||
|
||||
# 替换 go module name
|
||||
!replace content
|
||||
exact go.mod
|
||||
module ultone => module {{ .MODULE_NAME }}
|
||||
EOF
|
||||
|
||||
# 替换 config name
|
||||
!replace content
|
||||
exact etc/config.json
|
||||
"name": "ult" => "name": "{{.PROJECT_NAME}}"
|
||||
EOF
|
||||
|
||||
# 生成 readme
|
||||
!generate
|
||||
readme.md
|
||||
# {{.PROJECT_NAME}}
|
||||
|
||||
### Run
|
||||
- `go run . --help`
|
||||
- `go run .`
|
||||
|
||||
### Build
|
||||
- `docker build -t {repo:tag} -f Dockerfile .`
|
||||
EOF
|
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
#FROM golang:1.20-alpine AS builder
|
||||
FROM repository.umisen.com/external/golang:latest AS builder
|
||||
|
||||
ENV GO111MODULE on
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GOOS linux
|
||||
ENV GOPROXY https://goproxy.io
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod download
|
||||
RUN go build -ldflags '-s -w' -o server .
|
||||
|
||||
#FROM alpine:latest
|
||||
FROM repository.umisen.com/external/alpine:latest
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && apk add curl
|
||||
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /data
|
||||
|
||||
COPY --from=builder /build/server /app/server
|
||||
COPY etc /app/etc
|
||||
|
||||
CMD ["/app/server", "-c", "/app/etc/config.json"]
|
56
deployment/database.dev.yml
Normal file
56
deployment/database.dev.yml
Normal file
@ -0,0 +1,56 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: "repository.umisen.com/external/redis:latest"
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- .data/redis:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
pgsql:
|
||||
image: "repository.umisen.com/external/postgres:latest"
|
||||
container_name: pgsql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ult
|
||||
POSTGRES_PASSWORD: ult@sonar
|
||||
POSTGRES_DB: ult
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- .data/pgsql:/var/lib/postgresql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
es:
|
||||
image: "repository.umisen.com/external/es:latest"
|
||||
container_name: es
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- bootstrap.memory_lock=true
|
||||
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
|
||||
- discovery.type=single-node
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
ports:
|
||||
- 9200:9200
|
||||
|
||||
kibana:
|
||||
image: "repository.umisen.com/external/kibana:latest"
|
||||
container_name: kibana
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ELASTICSEARCH_HOSTS: http://elk_es:9200
|
||||
I18N_LOCALE: zh-CN
|
||||
ports:
|
||||
- 5601:5601
|
8
deployment/run.sh
Normal file
8
deployment/run.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#/bin/bash
|
||||
|
||||
VERSION="v$(date +'%y.%m.%d')-r1"
|
||||
echo "version: $VERSION"
|
||||
|
||||
docker build -t repository.umisen.com/{project_folder}/{project_name}:$VERSION -f Dockerfile .
|
||||
docker push repository.umisen.com/{project_folder}/{project_name}:$VERSION
|
||||
docker start -d --name {your_project_name} --restart unless-stopped -p xx_port:xx_port -v xx_path:xx_path repository.umisen.com/{project_folder}/{project_name}:$VERSION
|
19
etc/config.json
Normal file
19
etc/config.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "esway",
|
||||
"listen": {
|
||||
"gateway": "0.0.0.0:8080",
|
||||
"dashboard": "0.0.0.0:8081"
|
||||
},
|
||||
"db": {
|
||||
"_uri": "postgres::host=pg.dev user=xx_user password=xx_password dbname=xx_database port=5432 sslmode=disable TimeZone=Asia/Shanghai",
|
||||
"uri": "sqlite::db.sqlite3"
|
||||
},
|
||||
"cache": {
|
||||
"uri": "lru::",
|
||||
"_uri": "memory::",
|
||||
"__uri": "redis::u:p@redis.dev:6379"
|
||||
},
|
||||
"endpoints": [
|
||||
"http://192.168.8.99:9200"
|
||||
]
|
||||
}
|
70
go.mod
Normal file
70
go.mod
Normal file
@ -0,0 +1,70 @@
|
||||
module esway
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
gitea.com/loveuer/gredis v1.0.0
|
||||
github.com/elastic/go-elasticsearch/v7 v7.17.10
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/jackc/pgtype v1.12.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.9
|
||||
github.com/loveuer/esgo2dump v0.3.3
|
||||
github.com/loveuer/nf v0.3.0
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/sirupsen/logrus v1.9.2
|
||||
github.com/spf13/cast v1.6.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/tdewolff/minify/v2 v2.20.16
|
||||
golang.org/x/crypto v0.27.0
|
||||
google.golang.org/grpc v1.50.0
|
||||
gorm.io/driver/mysql v1.4.5
|
||||
gorm.io/driver/postgres v1.4.4
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.11 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
392
go.sum
Normal file
392
go.sum
Normal file
@ -0,0 +1,392 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
gitea.com/loveuer/gredis v1.0.0 h1:fbRS8YZObcp1KV1KGj8pDpIj1WrI0W8pwU9Ny/2fJys=
|
||||
gitea.com/loveuer/gredis v1.0.0/go.mod h1:TQlubgDiyNTRXqASd/XIUrqPBLj9NZRR2DmV3V2ZyMY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
|
||||
github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
||||
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU=
|
||||
github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/loveuer/esgo2dump v0.3.3 h1:/AidoaFV7bDRyT1ycyBKs4XGmyVs2ShaUKrpEBiUWkM=
|
||||
github.com/loveuer/esgo2dump v0.3.3/go.mod h1:thZvfsO0kd7Ck3TA0jc9rRc4CuIa4Iuiq6tF3tCqXEY=
|
||||
github.com/loveuer/nf v0.3.0 h1:ITPzgeiO32OstVJzrg/i3kmKmv+WeYx2anYHVRZtIv8=
|
||||
github.com/loveuer/nf v0.3.0/go.mod h1:M6reF17/kJBis30H4DxR5hrtgo/oJL4AV4cBe4HzJLw=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tdewolff/minify/v2 v2.20.16 h1:/C8dtRkxLTIyUlKlBz46gDiktCrE8a6+c1gTrnPFz+U=
|
||||
github.com/tdewolff/minify/v2 v2.20.16/go.mod h1:/FvxV9KaTrFu35J9I2FhRvWSBxcHj8sDSdwBFh5voxM=
|
||||
github.com/tdewolff/parse/v2 v2.7.11 h1:v+W45LnzmjndVlfqPCT5gGjAAZKd1GJGOPJveTIkBY8=
|
||||
github.com/tdewolff/parse/v2 v2.7.11/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU=
|
||||
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.5 h1:u1lytId4+o9dDaNcPCFzNv7h6wvmc92UjNk3z8enSBU=
|
||||
gorm.io/driver/mysql v1.4.5/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.4.4 h1:zt1fxJ+C+ajparn0SteEnkoPg0BQ6wOWXEQ99bteAmw=
|
||||
gorm.io/driver/postgres v1.4.4/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
|
||||
gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
29
httptest/user.http
Normal file
29
httptest/user.http
Normal file
@ -0,0 +1,29 @@
|
||||
### login
|
||||
POST http://127.0.0.1:8080/api/user/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
|
||||
### verify login state
|
||||
GET http://127.0.0.1:8080/api/user/auth/login
|
||||
|
||||
### change self password
|
||||
POST http://127.0.0.1:8080/api/user/update
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"old_password": "123456",
|
||||
"new_password": "654321@AaBbCc"
|
||||
}
|
||||
|
||||
### relogin with new password
|
||||
POST http://127.0.0.1:8080/api/user/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "654321@AaBbCc"
|
||||
}
|
70
internal/api/api.go
Normal file
70
internal/api/api.go
Normal file
@ -0,0 +1,70 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"esway/internal/handler"
|
||||
"esway/internal/middleware/auth"
|
||||
"esway/internal/middleware/logger"
|
||||
"esway/internal/middleware/oplog"
|
||||
"esway/internal/middleware/privilege"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
)
|
||||
|
||||
func initApp(ctx context.Context) *nf.App {
|
||||
engine := nf.New(nf.Config{DisableLogger: true, DisableBanner: true, DisableMessagePrint: true})
|
||||
engine.Use(logger.New())
|
||||
|
||||
// todo: add project prefix, if you need
|
||||
// for example: app := engine.Group("/api/{project}")
|
||||
app := engine.Group("/api")
|
||||
app.Get("/available", func() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
now := time.Now()
|
||||
return resp.Resp200(c, nf.Map{"ok": opt.OK, "start": opt.Start, "now": now, "duration": fmt.Sprint(now.Sub(opt.Start))})
|
||||
}
|
||||
}())
|
||||
|
||||
{
|
||||
api := app.Group("/user")
|
||||
api.Post("/auth/login", oplog.NewOpLog(ctx), handler.AuthLogin)
|
||||
api.Get("/auth/login", auth.NewAuth(), handler.AuthVerify)
|
||||
api.Post("/auth/logout", auth.NewAuth(), oplog.NewOpLog(ctx), handler.AuthLogout)
|
||||
|
||||
api.Post("/update", auth.NewAuth(), handler.UserUpdate)
|
||||
|
||||
mng := api.Group("/manage")
|
||||
mng.Use(auth.NewAuth(), privilege.Verify(
|
||||
privilege.RelationAnd,
|
||||
model.PrivilegeUserManage,
|
||||
))
|
||||
|
||||
mng.Get("/user/list", handler.ManageUserList)
|
||||
mng.Post("/user/create", oplog.NewOpLog(ctx), handler.ManageUserCreate)
|
||||
mng.Post("/user/update", oplog.NewOpLog(ctx), handler.ManageUserUpdate)
|
||||
mng.Post("/user/delete", oplog.NewOpLog(ctx), handler.ManageUserDelete)
|
||||
}
|
||||
|
||||
{
|
||||
api := app.Group("/log")
|
||||
api.Use(auth.NewAuth(), privilege.Verify(privilege.RelationAnd, model.PrivilegeOpLog))
|
||||
api.Get("/category/list", handler.LogCategories())
|
||||
api.Get("/content/list", handler.LogList)
|
||||
}
|
||||
|
||||
{
|
||||
// todo: 替换 xxx
|
||||
// todo: 这里写你的模块和接口
|
||||
api := app.Group("/xxx")
|
||||
api.Use(auth.NewAuth())
|
||||
_ = api // todo: 添加自己的接口后删除该行
|
||||
}
|
||||
|
||||
return engine
|
||||
}
|
45
internal/api/start.go
Normal file
45
internal/api/start.go
Normal file
@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
app := initApp(ctx)
|
||||
ready := make(chan bool)
|
||||
|
||||
ln, err := net.Listen("tcp", opt.Cfg.Listen.Dashboard)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api.MustStart: net listen tcp address=%v err=%v", opt.Cfg.Listen.Dashboard, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
|
||||
fmt.Printf("esway: dashboard listen at %s\n", opt.Cfg.Listen.Dashboard)
|
||||
if err = app.RunListener(ln); err != nil {
|
||||
log.Panic("api.MustStart: app run err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
<-ctx.Done()
|
||||
if err = app.Shutdown(tool.Timeout(2)); err != nil {
|
||||
log.Error("api.MustStart: app shutdown err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
return nil
|
||||
}
|
78
internal/cmd/cli.go
Normal file
78
internal/cmd/cli.go
Normal file
@ -0,0 +1,78 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
|
||||
"esway/internal/log"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/unix"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cliCommand = &cobra.Command{
|
||||
Use: "cli",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug(cmd.Context(), "[cmd.cli] svc address: %s", svc)
|
||||
|
||||
uri, err := url.Parse(svc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cliClient, err = rpc.Dial(uri.Scheme, uri.Host+uri.Path); err != nil {
|
||||
return fmt.Errorf("rpc dial [%s] [%s] err: %w", uri.Scheme, uri.Host+uri.Path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
log.Debug(cmd.Context(), "[cli] start run cli... all args: %v", args)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
svc string
|
||||
cliClient *rpc.Client
|
||||
)
|
||||
|
||||
func initCli() {
|
||||
cliCommand.PersistentFlags().StringVar(&svc, "svc", opt.RpcAddress, "server unix listen address")
|
||||
cliCommand.AddCommand(&cobra.Command{
|
||||
Use: "set",
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
log.Debug(cmd.Context(), "[cli.set] all args: %v", args)
|
||||
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("at least 2 args required")
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "debug":
|
||||
out := &unix.Resp[bool]{}
|
||||
in := &unix.SettingReq{Debug: false}
|
||||
switch args[1] {
|
||||
case "true":
|
||||
in.Debug = true
|
||||
case "false":
|
||||
default:
|
||||
return fmt.Errorf("unknown debug value")
|
||||
}
|
||||
|
||||
if err = cliClient.Call("svc.Setting", in, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(cmd.Context(), out.Msg)
|
||||
default:
|
||||
return fmt.Errorf("unknown set variable(debug is available now)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
37
internal/cmd/execute.go
Normal file
37
internal/cmd/execute.go
Normal file
@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"esway/internal/api"
|
||||
"esway/internal/controller"
|
||||
"esway/internal/database/cache"
|
||||
"esway/internal/database/db"
|
||||
"esway/internal/gateway"
|
||||
"esway/internal/log"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
)
|
||||
|
||||
func execute(ctx context.Context) error {
|
||||
tool.Must(opt.Init(opt.Cfg.Config))
|
||||
tool.Must(db.Init(ctx, opt.Cfg.DB.Uri))
|
||||
tool.Must(cache.Init())
|
||||
|
||||
tool.Must(model.Init(db.Default.Session()))
|
||||
tool.Must(controller.Init(ctx))
|
||||
tool.Must(gateway.Start(ctx))
|
||||
tool.Must(api.Start(ctx))
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
log.Warn(ctx, "received quit signal...(2s)")
|
||||
<-tool.Timeout(2).Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context) error {
|
||||
return rootCommand.ExecuteContext(ctx)
|
||||
}
|
13
internal/cmd/init.go
Normal file
13
internal/cmd/init.go
Normal file
@ -0,0 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
time.Local = time.FixedZone("CST", 8*3600)
|
||||
|
||||
initCli()
|
||||
|
||||
initRoot(cliCommand)
|
||||
}
|
26
internal/cmd/root.go
Normal file
26
internal/cmd/root.go
Normal file
@ -0,0 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"esway/internal/opt"
|
||||
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCommand = &cobra.Command{
|
||||
Use: "esway",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if opt.Cfg.Debug {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return execute(cmd.Context())
|
||||
},
|
||||
}
|
||||
|
||||
func initRoot(cmds ...*cobra.Command) {
|
||||
rootCommand.PersistentFlags().BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode")
|
||||
rootCommand.PersistentFlags().StringVarP(&opt.Cfg.Config, "config", "c", "etc/config.json", "config json file path")
|
||||
rootCommand.AddCommand(cmds...)
|
||||
}
|
14
internal/controller/impl.go
Normal file
14
internal/controller/impl.go
Normal file
@ -0,0 +1,14 @@
|
||||
package controller
|
||||
|
||||
import "context"
|
||||
|
||||
var (
|
||||
// UserController todo: 可以实现自己的 controller
|
||||
UserController userController
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
UserController = uc{}
|
||||
|
||||
return nil
|
||||
}
|
164
internal/controller/user.go
Normal file
164
internal/controller/user.go
Normal file
@ -0,0 +1,164 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"esway/internal/database/cache"
|
||||
"esway/internal/database/db"
|
||||
"esway/internal/log"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type userController interface {
|
||||
GetUser(ctx context.Context, id uint64) (*model.User, error)
|
||||
GetUserByToken(ctx context.Context, token string) (*model.User, error)
|
||||
CacheUser(ctx context.Context, user *model.User) error
|
||||
CacheToken(ctx context.Context, token string, user *model.User) error
|
||||
RmToken(ctx context.Context, token string) error
|
||||
RmUserCache(ctx context.Context, id uint64) error
|
||||
DeleteUser(ctx context.Context, target *model.User) error
|
||||
}
|
||||
|
||||
type uc struct{}
|
||||
|
||||
var _ userController = (*uc)(nil)
|
||||
|
||||
func (u uc) GetUser(ctx context.Context, id uint64) (*model.User, error) {
|
||||
var (
|
||||
err error
|
||||
target = new(model.User)
|
||||
key = fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if bs, err = cache.Client.Get(tool.Timeout(3), key); err != nil {
|
||||
log.Warn(ctx, "controller.GetUser: get user by cache key=%s err=%v", key, err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, target); err != nil {
|
||||
log.Warn(ctx, "controller.GetUser: json unmarshal key=%s by=%s err=%v", key, string(bs), err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
ByDB:
|
||||
if err = db.Default.Session(tool.Timeout(3)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Take(target).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// tips: 公开项目需要考虑击穿处理
|
||||
return target, resp.NewError(400, "目标不存在", err, nil)
|
||||
}
|
||||
|
||||
return target, resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.CacheUser(ctx, target); err != nil {
|
||||
log.Warn(ctx, "controller.GetUser: cache user key=%s err=%v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (u uc) GetUserByToken(ctx context.Context, token string) (*model.User, error) {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return nil, fmt.Errorf("controller.GetUserByToken: jwt token invalid, token=%s", token)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
bs, err := cache.Client.Get(tool.Timeout(3), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug(ctx, "controller.GetUserByToken: key=%s cache bytes=%s", key, string(bs))
|
||||
|
||||
userId := cast.ToUint64(string(bs))
|
||||
if userId == 0 {
|
||||
return nil, fmt.Errorf("controller.GetUserByToken: bs=%s cast to uint64 err", string(bs))
|
||||
}
|
||||
|
||||
var op *model.User
|
||||
|
||||
if op, err = u.GetUser(ctx, userId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func (u uc) CacheUser(ctx context.Context, target *model.User) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, target.Id)
|
||||
return cache.Client.Set(tool.Timeout(3), key, target)
|
||||
}
|
||||
|
||||
func (u uc) CacheToken(ctx context.Context, token string, user *model.User) error {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return fmt.Errorf("controller.CacheToken: jwt token invalid")
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
return cache.Client.SetEx(tool.Timeout(3), key, user.Id, opt.TokenTimeout)
|
||||
}
|
||||
|
||||
func (u uc) RmToken(ctx context.Context, token string) error {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return fmt.Errorf("controller.CacheToken: jwt token invalid")
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
return cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
func (u uc) RmUserCache(ctx context.Context, id uint64) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
return cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
func (u uc) DeleteUser(ctx context.Context, target *model.User) error {
|
||||
var (
|
||||
err error
|
||||
now = time.Now()
|
||||
username = fmt.Sprintf("%s@%d", target.Username, now.UnixMilli())
|
||||
)
|
||||
|
||||
if err = db.Default.Session(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", target.Id).
|
||||
Updates(map[string]any{
|
||||
"deleted_at": now.UnixMilli(),
|
||||
"username": username,
|
||||
}).Error; err != nil {
|
||||
return resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.RmUserCache(ctx, target.Id); err != nil {
|
||||
log.Warn(ctx, "controller.DeleteUser: rm user=%d cache err=%v", target.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
111
internal/database/cache/cache_lru.go
vendored
Normal file
111
internal/database/cache/cache_lru.go
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"esway/internal/interfaces"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
_ "github.com/hashicorp/golang-lru/v2/expirable"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_lru)(nil)
|
||||
|
||||
type _lru struct {
|
||||
client *expirable.LRU[string, *_lru_value]
|
||||
}
|
||||
|
||||
type _lru_value struct {
|
||||
duration time.Duration
|
||||
last time.Time
|
||||
bs []byte
|
||||
}
|
||||
|
||||
func (l *_lru) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, ok := l.client.Get(key)
|
||||
if !ok {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
if v.duration == 0 {
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
if time.Now().Sub(v.last) > v.duration {
|
||||
l.client.Remove(key)
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
func (l *_lru) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, ok := l.client.Get(key)
|
||||
if !ok {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
if v.duration == 0 {
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if now.Sub(v.last) > v.duration {
|
||||
l.client.Remove(key)
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: now,
|
||||
bs: v.bs,
|
||||
})
|
||||
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
func (l *_lru) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: 0,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) Del(ctx context.Context, keys ...string) error {
|
||||
for _, key := range keys {
|
||||
l.client.Remove(key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLRUCache() (interfaces.Cacher, error) {
|
||||
client := expirable.NewLRU[string, *_lru_value](1024*1024, nil, 0)
|
||||
|
||||
return &_lru{client: client}, nil
|
||||
}
|
75
internal/database/cache/cache_memory.go
vendored
Normal file
75
internal/database/cache/cache_memory.go
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"esway/internal/interfaces"
|
||||
|
||||
"gitea.com/loveuer/gredis"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_mem)(nil)
|
||||
|
||||
type _mem struct {
|
||||
client *gredis.Gredis
|
||||
}
|
||||
|
||||
func (m *_mem) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, err := m.client.Get(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, err := m.client.GetEx(key, duration)
|
||||
if err != nil {
|
||||
if errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.Set(key, bs)
|
||||
}
|
||||
|
||||
func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.SetEx(key, bs, duration)
|
||||
}
|
||||
|
||||
func (m *_mem) Del(ctx context.Context, keys ...string) error {
|
||||
m.client.Delete(keys...)
|
||||
return nil
|
||||
}
|
63
internal/database/cache/cache_redis.go
vendored
Normal file
63
internal/database/cache/cache_redis.go
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
)
|
||||
|
||||
type _redis struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
result, err := r.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
result, err := r.client.GetEx(ctx, key, duration).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.Set(ctx, key, bs, redis.KeepTTL).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.SetEX(ctx, key, bs, duration).Result()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) Del(ctx context.Context, keys ...string) error {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
37
internal/database/cache/client.go
vendored
Normal file
37
internal/database/cache/client.go
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"esway/internal/interfaces"
|
||||
)
|
||||
|
||||
var Client interfaces.Cacher
|
||||
|
||||
type encoded_value interface {
|
||||
MarshalBinary() ([]byte, error)
|
||||
}
|
||||
|
||||
type decoded_value interface {
|
||||
UnmarshalBinary(bs []byte) error
|
||||
}
|
||||
|
||||
func handleValue(value any) ([]byte, error) {
|
||||
var (
|
||||
bs []byte
|
||||
err error
|
||||
)
|
||||
|
||||
switch value.(type) {
|
||||
case []byte:
|
||||
return value.([]byte), nil
|
||||
}
|
||||
|
||||
if imp, ok := value.(encoded_value); ok {
|
||||
bs, err = imp.MarshalBinary()
|
||||
} else {
|
||||
bs, err = json.Marshal(value)
|
||||
}
|
||||
|
||||
return bs, err
|
||||
}
|
7
internal/database/cache/error.go
vendored
Normal file
7
internal/database/cache/error.go
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package cache
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorKeyNotFound = errors.New("key not found")
|
||||
)
|
69
internal/database/cache/init.go
vendored
Normal file
69
internal/database/cache/init.go
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
|
||||
"gitea.com/loveuer/gredis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
var err error
|
||||
|
||||
strs := strings.Split(opt.Cfg.Cache.Uri, "::")
|
||||
|
||||
switch strs[0] {
|
||||
case "memory":
|
||||
gc := gredis.NewGredis(1024 * 1024)
|
||||
Client = &_mem{client: gc}
|
||||
case "lru":
|
||||
if Client, err = newLRUCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "redis":
|
||||
var (
|
||||
ins *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
if len(strs) != 2 {
|
||||
return fmt.Errorf("cache.Init: invalid cache uri: %s", opt.Cfg.Cache.Uri)
|
||||
}
|
||||
|
||||
uri := strs[1]
|
||||
|
||||
if !strings.Contains(uri, "://") {
|
||||
uri = fmt.Sprintf("redis://%s", uri)
|
||||
}
|
||||
|
||||
if ins, err = url.Parse(uri); err != nil {
|
||||
return fmt.Errorf("cache.Init: url parse cache uri: %s, err: %s", opt.Cfg.Cache.Uri, err.Error())
|
||||
}
|
||||
|
||||
addr := ins.Host
|
||||
username := ins.User.Username()
|
||||
password, _ := ins.User.Password()
|
||||
|
||||
var rc *redis.Client
|
||||
rc = redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
if err = rc.Ping(tool.Timeout(5)).Err(); err != nil {
|
||||
return fmt.Errorf("cache.Init: redis ping err: %s", err.Error())
|
||||
}
|
||||
|
||||
Client = &_redis{client: rc}
|
||||
default:
|
||||
return fmt.Errorf("cache type %s not support", strs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
52
internal/database/db/client.go
Normal file
52
internal/database/db/client.go
Normal file
@ -0,0 +1,52 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var Default *Client
|
||||
|
||||
type DBType string
|
||||
|
||||
const (
|
||||
DBTypeSqlite = "sqlite"
|
||||
DBTypeMysql = "mysql"
|
||||
DBTypePostgres = "postgres"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
cli *gorm.DB
|
||||
dbType DBType
|
||||
}
|
||||
|
||||
func (c *Client) Type() DBType {
|
||||
return c.dbType
|
||||
}
|
||||
|
||||
func (c *Client) Session(ctxs ...context.Context) *gorm.DB {
|
||||
var ctx context.Context
|
||||
if len(ctxs) > 0 && ctxs[0] != nil {
|
||||
ctx = ctxs[0]
|
||||
} else {
|
||||
ctx = tool.Timeout(30)
|
||||
}
|
||||
|
||||
session := c.cli.Session(&gorm.Session{Context: ctx})
|
||||
|
||||
if opt.Cfg.Debug {
|
||||
session = session.Debug()
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
d, _ := c.cli.DB()
|
||||
d.Close()
|
||||
}
|
8
internal/database/db/db_test.go
Normal file
8
internal/database/db/db_test.go
Normal file
@ -0,0 +1,8 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
}
|
55
internal/database/db/new.go
Normal file
55
internal/database/db/new.go
Normal file
@ -0,0 +1,55 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, uri string) (*Client, error) {
|
||||
parts := strings.SplitN(uri, "::", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("db.Init: opt db uri invalid: %s", uri)
|
||||
}
|
||||
|
||||
c := &Client{}
|
||||
|
||||
var (
|
||||
err error
|
||||
dsn = parts[1]
|
||||
)
|
||||
|
||||
switch parts[0] {
|
||||
case "sqlite":
|
||||
c.dbType = DBTypeSqlite
|
||||
c.cli, err = gorm.Open(sqlite.Open(dsn))
|
||||
case "mysql":
|
||||
c.dbType = DBTypeMysql
|
||||
c.cli, err = gorm.Open(mysql.Open(dsn))
|
||||
case "postgres":
|
||||
c.dbType = DBTypePostgres
|
||||
c.cli, err = gorm.Open(postgres.Open(dsn))
|
||||
default:
|
||||
return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", parts[0])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", parts[0], dsn, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Init(ctx context.Context, uri string) (err error) {
|
||||
if Default, err = New(ctx, uri); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
39
internal/database/es/client.go
Normal file
39
internal/database/es/client.go
Normal file
@ -0,0 +1,39 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"esway/internal/tool"
|
||||
|
||||
elastic "github.com/elastic/go-elasticsearch/v7"
|
||||
"github.com/loveuer/esgo2dump/xes/es7"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
var Default *elastic.Client
|
||||
|
||||
func New(ctx context.Context, uri string) (*elastic.Client, error) {
|
||||
var (
|
||||
err error
|
||||
client *elastic.Client
|
||||
ins *url.URL
|
||||
)
|
||||
|
||||
if ins, err = url.Parse(uri); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug("es.InitClient url parse uri: %s, result: %+v", uri, ins)
|
||||
|
||||
if client, err = es7.NewClient(tool.Timeout(10), ins); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func Init(ctx context.Context, uri string) (err error) {
|
||||
Default, err = New(ctx, uri)
|
||||
return err
|
||||
}
|
1
internal/database/es/raw.go
Normal file
1
internal/database/es/raw.go
Normal file
@ -0,0 +1 @@
|
||||
package es
|
63
internal/gateway/gateway.go
Normal file
63
internal/gateway/gateway.go
Normal file
@ -0,0 +1,63 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"esway/internal/middleware/analysis"
|
||||
"esway/internal/middleware/logger"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
ch := make(chan struct{})
|
||||
app := nf.New(nf.Config{
|
||||
BodyLimit: 4 * 1024 * 1024,
|
||||
DisableLogger: true,
|
||||
DisableBanner: true,
|
||||
DisableMessagePrint: true,
|
||||
})
|
||||
|
||||
h, err := proxy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.Use(logger.New(logger.Config{IgnoreFn: func(c *nf.Ctx) bool {
|
||||
path := c.Path()
|
||||
if strings.HasPrefix(path, "/.") || strings.HasPrefix(path, "/_") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}}))
|
||||
app.Use(analysis.New())
|
||||
|
||||
app.Any("/*any", h)
|
||||
|
||||
ln, err := net.Listen("tcp", opt.Cfg.Listen.Gateway)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gateway listen at %s err: %s", opt.Cfg.Listen.Gateway, err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
ch <- struct{}{}
|
||||
fmt.Printf("esway: gateway listen at %s\n", opt.Cfg.Listen.Gateway)
|
||||
_ = app.RunListener(ln)
|
||||
}()
|
||||
<-ch
|
||||
|
||||
go func() {
|
||||
ch <- struct{}{}
|
||||
<-ctx.Done()
|
||||
_ = app.Shutdown(tool.Timeout(2))
|
||||
}()
|
||||
<-ch
|
||||
|
||||
return nil
|
||||
}
|
51
internal/gateway/proxy.go
Normal file
51
internal/gateway/proxy.go
Normal file
@ -0,0 +1,51 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
|
||||
"esway/internal/log"
|
||||
"esway/internal/opt"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
)
|
||||
|
||||
func proxy(ctx context.Context) (nf.HandlerFunc, error) {
|
||||
if len(opt.Cfg.Endpoints) == 0 {
|
||||
return nil, fmt.Errorf("gateway: 必须要指定 elasticsearch endpoints")
|
||||
}
|
||||
|
||||
urls := make([]*url.URL, 0)
|
||||
for _, item := range opt.Cfg.Endpoints {
|
||||
ins, err := url.Parse(item)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "gateway: endpoint invalid, endpoint = %s, err = %s", item, err.Error())
|
||||
}
|
||||
|
||||
urls = append(urls, ins)
|
||||
}
|
||||
|
||||
if len(urls) == 0 {
|
||||
return nil, fmt.Errorf("gateway: no valid elasticsearch endpoint")
|
||||
}
|
||||
|
||||
svcs := make([]*httputil.ReverseProxy, len(urls))
|
||||
for idx, item := range urls {
|
||||
svcs[idx] = httputil.NewSingleHostReverseProxy(item)
|
||||
}
|
||||
|
||||
var (
|
||||
round int64 = 0
|
||||
length = int64(len(svcs))
|
||||
)
|
||||
return func(c *nf.Ctx) error {
|
||||
svc := svcs[atomic.SwapInt64(&round, (round+1)%length)]
|
||||
|
||||
svc.ServeHTTP(c.Writer, c.Request)
|
||||
|
||||
return nil
|
||||
}, nil
|
||||
}
|
103
internal/handler/log.go
Normal file
103
internal/handler/log.go
Normal file
@ -0,0 +1,103 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"esway/internal/database/db"
|
||||
"esway/internal/log"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/sqlType"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
)
|
||||
|
||||
func LogCategories() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
return resp.Resp200(c, model.OpLogType(0).All())
|
||||
}
|
||||
}
|
||||
|
||||
func LogList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
UserIds []uint64 `query:"user_ids"`
|
||||
Types sqlType.NumSlice[model.OpLogType] `query:"types"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
list = make([]*model.OpLog, 0)
|
||||
total int
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size <= 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, req, "参数过大")
|
||||
}
|
||||
|
||||
txCount := op.Role.Where(db.Default.Session(tool.Timeout(3)).
|
||||
Model(&model.OpLog{}).
|
||||
Select("COUNT(`op_logs`.`id`)").
|
||||
Joins("LEFT JOIN users ON `users`.`id` = `op_logs`.`user_id`"))
|
||||
txGet := op.Role.Where(db.Default.Session(tool.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Joins("LEFT JOIN users ON `users`.`id` = `op_logs`.`user_id`"))
|
||||
|
||||
if len(req.UserIds) != 0 {
|
||||
txCount = txCount.Where("op_logs.user_id IN ?", req.UserIds)
|
||||
txGet = txGet.Where("op_logs.user_id IN ?", req.UserIds)
|
||||
}
|
||||
|
||||
if len(req.Types) != 0 {
|
||||
txCount = txCount.Where("op_logs.type IN ?", req.Types)
|
||||
txGet = txGet.Where("op_logs.type IN ?", req.Types)
|
||||
}
|
||||
|
||||
if err = txCount.
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = txGet.
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Order("`op_logs`.`created_at` DESC").
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
for _, logItem := range list {
|
||||
m := make(map[string]any)
|
||||
if err = logItem.Content.Bind(&m); err != nil {
|
||||
log.Warn(c.Context(), "handler.LogList: log=%d content=%v bind map[string]any err=%v", logItem.Id, logItem.Content, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if logItem.HTML, err = logItem.Type.Render(m); err != nil {
|
||||
log.Warn(c.Context(), "handler.LogList: log=%d template=%s render map=%+v err=%v", logItem.Id, logItem.Type.Template(), m, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug(c.Context(), "handler.LogList: log=%d render map=%+v string=%s", logItem.Id, m, logItem.HTML)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
585
internal/handler/user.go
Normal file
585
internal/handler/user.go
Normal file
@ -0,0 +1,585 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"esway/internal/controller"
|
||||
"esway/internal/database/cache"
|
||||
"esway/internal/database/db"
|
||||
"esway/internal/middleware/oplog"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/sqlType"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func AuthLogin(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
target = new(model.User)
|
||||
token string
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = db.Default.Session(tool.Timeout(3)).
|
||||
Model(&model.User{}).
|
||||
Where("username = ?", req.Username).
|
||||
Where("deleted_at = 0").
|
||||
Take(target).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return resp.Resp400(c, err.Error(), "用户名或密码错误")
|
||||
}
|
||||
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if !tool.ComparePassword(req.Password, target.Password) {
|
||||
return resp.Resp400(c, nil, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, nil, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.CacheUser(c.Context(), target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if token, err = target.JwtEncode(); err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.CacheToken(c.Context(), token, target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if !opt.MultiLogin {
|
||||
var (
|
||||
last = fmt.Sprintf("%s:user:last_token:%d", opt.CachePrefix, target.Id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
// 获取之前的 token
|
||||
if bs, err = cache.Client.Get(tool.Timeout(3), last); err == nil {
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, string(bs))
|
||||
_ = cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
// 删掉之前的 token
|
||||
if len(bs) > 0 {
|
||||
_ = controller.UserController.RmToken(c.Context(), string(bs))
|
||||
}
|
||||
|
||||
// 将当前的 token 存入 last_token
|
||||
if err = cache.Client.Set(tool.Timeout(3), last, token); err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("Set-Cookie", fmt.Sprintf("%s=%s; Path=/", opt.CookieName, token))
|
||||
c.Locals("user", target)
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeLogin, Content: map[string]any{
|
||||
"time": now.UnixMilli(),
|
||||
"ip": c.IP(true),
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nf.Map{"token": token, "user": target})
|
||||
}
|
||||
|
||||
func AuthVerify(c *nf.Ctx) error {
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
token, ok := c.Locals("token").(string)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"token": token, "user": op})
|
||||
}
|
||||
|
||||
func AuthLogout(c *nf.Ctx) error {
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
_ = controller.UserController.RmUserCache(c.Context(), op.Id)
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{
|
||||
Type: model.OpLogTypeLogout,
|
||||
Content: map[string]any{
|
||||
"time": time.Now().UnixMilli(),
|
||||
"ip": c.IP(),
|
||||
},
|
||||
})
|
||||
|
||||
return resp.Resp200(c, nil)
|
||||
}
|
||||
|
||||
func UserUpdate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Password string `gorm:"column:password"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
req = new(Req)
|
||||
user *model.User
|
||||
m = new(Model)
|
||||
updates = make(map[string]any)
|
||||
changes = make(map[string]any)
|
||||
)
|
||||
|
||||
if user, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(&changes); err != nil {
|
||||
return resp.Resp400(c, err)
|
||||
}
|
||||
|
||||
if _, ok = changes["nickname"]; ok {
|
||||
updates["nickname"] = req.Nickname
|
||||
}
|
||||
|
||||
if req.OldPassword != "" && req.NewPassword != "" {
|
||||
if err = tool.CheckPassword(req.NewPassword); err != nil {
|
||||
return resp.Resp400(c, req, err.Error())
|
||||
}
|
||||
|
||||
if err = db.Default.Session(tool.Timeout(3)).
|
||||
Select("password").
|
||||
Model(&model.User{}).
|
||||
Where("username = ?", user.Username).
|
||||
Where("deleted_at = 0").
|
||||
Take(m).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if !tool.ComparePassword(req.OldPassword, m.Password) {
|
||||
return resp.Resp400(c, nil, "原密码错误")
|
||||
}
|
||||
|
||||
updates["password"] = tool.NewPassword(req.NewPassword)
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
return resp.Resp400(c, nf.Map{"req": req, "reason": "nothing to update"}, "没有需要更新的内容")
|
||||
}
|
||||
|
||||
if err = db.Default.Session(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", user.Id).
|
||||
Updates(updates).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if _, ok = updates["password"]; ok {
|
||||
_ = controller.UserController.RmUserCache(c.Context(), user.Id)
|
||||
c.SetHeader("Set-Cookie", fmt.Sprintf("%s=;Path=/", opt.CookieName))
|
||||
return c.Redirect(opt.LoginURL, http.StatusFound)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nil, "修改成功")
|
||||
}
|
||||
|
||||
func ManageUserList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
Keyword string `query:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
list = make([]*model.User, 0)
|
||||
total = 0
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size == 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, nf.Map{"msg": "size over max", "max": opt.MaxSize})
|
||||
}
|
||||
|
||||
txList := op.Role.Where(db.Default.Session(tool.Timeout(10)).
|
||||
Model(&model.User{}).
|
||||
Where("deleted_at = 0"))
|
||||
txCount := op.Role.Where(db.Default.Session(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Select("COUNT(id)").
|
||||
Where("deleted_at = 0"))
|
||||
|
||||
if req.Keyword != "" {
|
||||
keyword := fmt.Sprintf("%%%s%%", req.Keyword)
|
||||
txList = txList.Where("username LIKE ?", keyword)
|
||||
txCount = txCount.Where("username LIKE ?", keyword)
|
||||
}
|
||||
|
||||
if err = txList.
|
||||
Order("updated_at DESC").
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = txCount.
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
||||
|
||||
func ManageUserCreate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
Comment string `json:"comment"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, req, err.Error())
|
||||
}
|
||||
|
||||
if req.Nickname == "" {
|
||||
req.Nickname = req.Username
|
||||
}
|
||||
|
||||
if req.Status.Code() == "unknown" {
|
||||
return resp.Resp400(c, req, "用户状态不正常")
|
||||
}
|
||||
|
||||
if req.Role == 0 {
|
||||
req.Role = model.RoleUser
|
||||
}
|
||||
|
||||
if req.ActiveAt == 0 {
|
||||
req.ActiveAt = now.UnixMilli()
|
||||
}
|
||||
|
||||
if req.Deadline == 0 {
|
||||
req.Deadline = now.AddDate(99, 0, 0).UnixMilli()
|
||||
}
|
||||
|
||||
newUser := &model.User{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
Username: req.Username,
|
||||
Password: tool.NewPassword(req.Password),
|
||||
Status: req.Status,
|
||||
Nickname: req.Nickname,
|
||||
Comment: req.Comment,
|
||||
Role: req.Role,
|
||||
Privileges: req.Privileges,
|
||||
CreatedById: op.Id,
|
||||
CreatedByName: op.CreatedByName,
|
||||
ActiveAt: op.ActiveAt,
|
||||
Deadline: op.Deadline,
|
||||
}
|
||||
|
||||
if err = newUser.IsValid(false); err != nil {
|
||||
return resp.Resp400(c, newUser, err.Error())
|
||||
}
|
||||
|
||||
if !newUser.Role.CanOP(op) {
|
||||
return resp.Resp403(c, newUser, "角色不符合")
|
||||
}
|
||||
|
||||
if err = db.Default.Session(tool.Timeout(5)).
|
||||
Create(newUser).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeCreateUser, Content: map[string]any{
|
||||
"target_id": newUser.Id,
|
||||
"target_username": newUser.Username,
|
||||
"target_nickname": newUser.Nickname,
|
||||
"target_status": newUser.Status.Label(),
|
||||
"target_role": newUser.Role.Label(),
|
||||
"target_privileges": lo.Map(newUser.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
"target_active_at": op.ActiveAt,
|
||||
"target_deadline": op.Deadline,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, newUser)
|
||||
}
|
||||
|
||||
func ManageUserUpdate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Comment string `json:"comment"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
type Change struct {
|
||||
Old any `json:"old"`
|
||||
New any `json:"new"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
target *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
rm = make(map[string]any)
|
||||
updates = make(map[string]any)
|
||||
changes = make(map[string]Change)
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = c.BodyParser(&rm); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
return resp.Resp400(c, "未指定目标用户")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(c.Context(), req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if op.Role < target.Role || ((op.Role == target.Role) && opt.RoleMustLess) {
|
||||
return resp.Resp403(c, req)
|
||||
}
|
||||
|
||||
if op.Id == req.Id {
|
||||
return resp.Resp403(c, req, "无法更新自己")
|
||||
}
|
||||
|
||||
if _, ok = rm["nickname"]; ok {
|
||||
if req.Nickname == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
updates["nickname"] = req.Nickname
|
||||
changes["昵称"] = Change{Old: target.Nickname, New: req.Nickname}
|
||||
}
|
||||
|
||||
if _, ok = rm["password"]; ok {
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
updates["password"] = tool.NewPassword(req.Password)
|
||||
changes["密码"] = Change{Old: "******", New: "******"}
|
||||
}
|
||||
|
||||
if _, ok = rm["status"]; ok {
|
||||
if req.Status.Code() == "unknown" {
|
||||
return resp.Resp400(c, req, "用户状态不符合")
|
||||
}
|
||||
|
||||
updates["status"] = req.Status
|
||||
changes["状态"] = Change{Old: target.Status.Label(), New: req.Status.Label()}
|
||||
}
|
||||
|
||||
if _, ok = rm["comment"]; ok {
|
||||
updates["comment"] = req.Comment
|
||||
changes["备注"] = Change{Old: target.Comment, New: req.Comment}
|
||||
}
|
||||
|
||||
if _, ok = rm["role"]; ok {
|
||||
if op.Role < req.Role || ((op.Role == req.Role) && opt.RoleMustLess) {
|
||||
return resp.Resp400(c, req, "用户角色不符合")
|
||||
}
|
||||
|
||||
updates["role"] = req.Role
|
||||
changes["角色"] = Change{Old: target.Role.Label(), New: req.Role.Label()}
|
||||
}
|
||||
|
||||
if _, ok = rm["privileges"]; ok {
|
||||
for _, val := range req.Privileges {
|
||||
if lo.IndexOf(op.Privileges, val) < 0 {
|
||||
return resp.Resp400(c, req, fmt.Sprintf("权限: %s 不符合", val.Label()))
|
||||
}
|
||||
}
|
||||
|
||||
changes["权限"] = Change{
|
||||
Old: lo.Map(target.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
New: lo.Map(req.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
}
|
||||
updates["privileges"] = req.Privileges
|
||||
}
|
||||
|
||||
if _, ok = rm["active_at"]; ok {
|
||||
updates["active_at"] = time.UnixMilli(req.ActiveAt).UnixMilli()
|
||||
changes["激活时间"] = Change{Old: target.ActiveAt, New: req.ActiveAt}
|
||||
}
|
||||
|
||||
if _, ok = rm["deadline"]; ok {
|
||||
updates["deadline"] = time.UnixMilli(req.Deadline).UnixMilli()
|
||||
changes["到期时间"] = Change{Old: target.Deadline, New: req.Deadline}
|
||||
}
|
||||
|
||||
updated := new(model.User)
|
||||
if err = db.Default.Session(tool.Timeout(5)).
|
||||
Model(updated).
|
||||
Clauses(clause.Returning{}).
|
||||
Where("id = ?", req.Id).
|
||||
Updates(updates).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.RmUserCache(c.Context(), req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeUpdateUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
"changes": changes,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, updated)
|
||||
}
|
||||
|
||||
func ManageUserDelete(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
target *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if req.Id == op.Id {
|
||||
return resp.Resp400(c, nil, "无法删除自己")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(c.Context(), req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if op.Role < target.Role || (op.Role == target.Role && opt.RoleMustLess) {
|
||||
return resp.Resp403(c, nil)
|
||||
}
|
||||
|
||||
if err = controller.UserController.DeleteUser(c.Context(), target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeDeleteUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nil, "删除成功")
|
||||
}
|
16
internal/interfaces/database.go
Normal file
16
internal/interfaces/database.go
Normal file
@ -0,0 +1,16 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cacher interface {
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error)
|
||||
// Set value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
Set(ctx context.Context, key string, value any) error
|
||||
// SetEx value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
SetEx(ctx context.Context, key string, value any, duration time.Duration) error
|
||||
Del(ctx context.Context, keys ...string) error
|
||||
}
|
11
internal/interfaces/enum.go
Normal file
11
internal/interfaces/enum.go
Normal file
@ -0,0 +1,11 @@
|
||||
package interfaces
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
14
internal/interfaces/logger.go
Normal file
14
internal/interfaces/logger.go
Normal file
@ -0,0 +1,14 @@
|
||||
package interfaces
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug(msg string, data ...any)
|
||||
Info(msg string, data ...any)
|
||||
Warn(msg string, data ...any)
|
||||
Error(msg string, data ...any)
|
||||
}
|
89
internal/invoke/client.go
Normal file
89
internal/invoke/client.go
Normal file
@ -0,0 +1,89 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"esway/internal/tool"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
SCHEME = "sonar"
|
||||
)
|
||||
|
||||
type Client[T any] struct {
|
||||
domain string
|
||||
endpoints []string
|
||||
fn func(grpc.ClientConnInterface) T
|
||||
opts []grpc.DialOption
|
||||
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func (c *Client[T]) Session() T {
|
||||
return c.fn(c.cc)
|
||||
}
|
||||
|
||||
var clients = &sync.Map{}
|
||||
|
||||
// NewClient
|
||||
/*
|
||||
* domain => Example: sonar_search
|
||||
* endpoints => Example: []string{"sonar_search:8080", "sonar_search:80801"} or []string{"10.10.10.10:32000", "10.10.10.10:32001"}
|
||||
* fn => Example: system.NewSystemSrvClient
|
||||
* opts => Example: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
*/
|
||||
func NewClient[T any](
|
||||
domain string,
|
||||
endpoints []string,
|
||||
fn func(grpc.ClientConnInterface) T,
|
||||
opts ...grpc.DialOption,
|
||||
) (*Client[T], error) {
|
||||
cached, ok := clients.Load(domain)
|
||||
if ok {
|
||||
if client, ok := cached.(*Client[T]); ok {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
resolved := resolver.State{Addresses: make([]resolver.Address, 0)}
|
||||
|
||||
locker.Lock()
|
||||
for _, item := range endpoints {
|
||||
resolved.Addresses = append(resolved.Addresses, resolver.Address{Addr: item})
|
||||
}
|
||||
ips[domain] = resolved
|
||||
locker.Unlock()
|
||||
|
||||
fullAddress := fmt.Sprintf("%s://%s", SCHEME, domain)
|
||||
|
||||
opts = append(opts,
|
||||
grpc.WithResolvers(myBuilder),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
|
||||
grpc.WithChainUnaryInterceptor(retryInterceptor(3, 3*time.Second)),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
|
||||
conn, err := grpc.DialContext(
|
||||
tool.Timeout(3),
|
||||
fullAddress,
|
||||
opts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client[T]{
|
||||
cc: conn,
|
||||
fn: fn,
|
||||
}
|
||||
|
||||
clients.Store(domain, c)
|
||||
|
||||
return c, nil
|
||||
}
|
82
internal/invoke/resolve.go
Normal file
82
internal/invoke/resolve.go
Normal file
@ -0,0 +1,82 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
scheme = "bifrost"
|
||||
)
|
||||
|
||||
type CustomBuilder struct{}
|
||||
|
||||
func (cb *CustomBuilder) Scheme() string {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func (cb *CustomBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
cr := &customResolver{
|
||||
cc: cc,
|
||||
target: target,
|
||||
}
|
||||
|
||||
cr.ResolveNow(resolver.ResolveNowOptions{})
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
type customResolver struct {
|
||||
sync.Mutex
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
ips map[string]string
|
||||
}
|
||||
|
||||
func (cr *customResolver) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
var (
|
||||
addrs = make([]resolver.Address, 0)
|
||||
hp []string
|
||||
)
|
||||
|
||||
cr.Lock()
|
||||
defer cr.Unlock()
|
||||
|
||||
if hp = strings.Split(cr.target.URL.Host, ":"); len(hp) >= 2 {
|
||||
if ip, ok := pool[hp[0]]; ok {
|
||||
addr := fmt.Sprintf("%s:%s", ip, hp[1])
|
||||
addrs = append(addrs, resolver.Address{Addr: addr})
|
||||
}
|
||||
}
|
||||
|
||||
_ = cr.cc.UpdateState(resolver.State{Addresses: addrs})
|
||||
}
|
||||
|
||||
func (cr *customResolver) Close() {}
|
||||
|
||||
var (
|
||||
cb = &CustomBuilder{}
|
||||
pool = make(map[string]string)
|
||||
)
|
||||
|
||||
func init() {
|
||||
resolver.Register(cb)
|
||||
}
|
||||
|
||||
type CustomDomain struct {
|
||||
Domain string
|
||||
IP string
|
||||
}
|
||||
|
||||
func NewCustomBuilder(cds ...CustomDomain) resolver.Builder {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
for _, cd := range cds {
|
||||
pool[cd.Domain] = cd.IP
|
||||
}
|
||||
|
||||
return cb
|
||||
}
|
43
internal/invoke/resolve_v2.go
Normal file
43
internal/invoke/resolve_v2.go
Normal file
@ -0,0 +1,43 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type Builder struct{}
|
||||
|
||||
func (b *Builder) Scheme() string {
|
||||
return SCHEME
|
||||
}
|
||||
|
||||
func (b *Builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
cr := &Resolver{
|
||||
cc: cc,
|
||||
target: target,
|
||||
}
|
||||
|
||||
cr.ResolveNow(resolver.ResolveNowOptions{})
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
logrus.Tracef("resolve_v2 ResolveNow => target: %s, %v", r.target.URL.Host, ips)
|
||||
_ = r.cc.UpdateState(ips[r.target.URL.Host])
|
||||
}
|
||||
|
||||
func (cr *Resolver) Close() {}
|
||||
|
||||
var (
|
||||
locker = &sync.Mutex{}
|
||||
myBuilder = &Builder{}
|
||||
ips = map[string]resolver.State{}
|
||||
)
|
43
internal/invoke/retry.go
Normal file
43
internal/invoke/retry.go
Normal file
@ -0,0 +1,43 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func retryInterceptor(maxAttempt int, interval time.Duration) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
|
||||
if maxAttempt == 0 {
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
duration := interval
|
||||
|
||||
for attempt := 1; attempt <= maxAttempt; attempt++ {
|
||||
|
||||
if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Unavailable {
|
||||
logrus.Debugf("Connection failed err: %v, retry %d after %fs", err, attempt, duration.Seconds())
|
||||
|
||||
time.Sleep(duration)
|
||||
duration *= 2
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil // 请求成功,不需要重试
|
||||
}
|
||||
|
||||
return fmt.Errorf("max retry attempts reached")
|
||||
}
|
||||
}
|
46
internal/log/log.go
Normal file
46
internal/log/log.go
Normal file
@ -0,0 +1,46 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/nf"
|
||||
ulog "github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
func _mix(ctx context.Context, msg string) string {
|
||||
if ctx == nil {
|
||||
return fmt.Sprintf("%s | %s", uuid.Must(uuid.NewV7()).String(), msg)
|
||||
}
|
||||
|
||||
traceId := ctx.Value(nf.TraceKey)
|
||||
if traceId == nil {
|
||||
return fmt.Sprintf("%s | %s", uuid.Must(uuid.NewV7()).String(), msg)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s | %s", traceId, msg)
|
||||
}
|
||||
|
||||
func Debug(ctx context.Context, msg string, data ...any) {
|
||||
ulog.Debug(_mix(ctx, msg), data...)
|
||||
}
|
||||
|
||||
func Info(ctx context.Context, msg string, data ...any) {
|
||||
ulog.Info(_mix(ctx, msg), data...)
|
||||
}
|
||||
|
||||
func Warn(ctx context.Context, msg string, data ...any) {
|
||||
ulog.Warn(_mix(ctx, msg), data...)
|
||||
}
|
||||
|
||||
func Error(ctx context.Context, msg string, data ...any) {
|
||||
ulog.Error(_mix(ctx, msg), data...)
|
||||
}
|
||||
|
||||
func Panic(ctx context.Context, msg string, data ...any) {
|
||||
ulog.Panic(_mix(ctx, msg), data...)
|
||||
}
|
||||
|
||||
func Fatal(ctx context.Context, msg string, data ...any) {
|
||||
ulog.Fatal(_mix(ctx, msg), data...)
|
||||
}
|
69
internal/middleware/analysis/new.go
Normal file
69
internal/middleware/analysis/new.go
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
analysis:
|
||||
|
||||
对访问 es 的 api 做一个初步的分类:
|
||||
- 是不是一次 client 的请求
|
||||
- 操作的 indixes 是哪些
|
||||
- 使用的对应的 es 的 api 是哪个?
|
||||
*/
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"esway/internal/log"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const LocalKey = "es"
|
||||
|
||||
func New() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
local := &model.ESReqRes{
|
||||
ClientRequest: false,
|
||||
Path: c.Path(),
|
||||
Method: c.Method(),
|
||||
Indixes: []string{},
|
||||
Api: model.ESApiUnknown,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
c.Locals(LocalKey, local)
|
||||
if opt.Cfg.Debug {
|
||||
log.Debug(c.Context(), "middleware.analysis: local = %#v", *local)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(local.Path) == 0 || strings.HasPrefix(local.Path, "/_") || strings.HasPrefix(local.Method, "/.") {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
paths := strings.Split(local.Path[1:], "/")
|
||||
|
||||
local.ClientRequest = true
|
||||
local.Indixes = lo.FilterMap(
|
||||
strings.Split(paths[0], ","),
|
||||
func(item string, idx int) (string, bool) {
|
||||
val := strings.TrimSpace(item)
|
||||
return val, val != ""
|
||||
},
|
||||
)
|
||||
|
||||
if len(paths) < 2 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
switch paths[1] {
|
||||
case "_doc":
|
||||
case "_search":
|
||||
case "_update":
|
||||
case "_update_by_query":
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
54
internal/middleware/auth/auth.go
Normal file
54
internal/middleware/auth/auth.go
Normal file
@ -0,0 +1,54 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"esway/internal/controller"
|
||||
"esway/internal/database/cache"
|
||||
"esway/internal/log"
|
||||
"esway/internal/opt"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
)
|
||||
|
||||
var tokenFunc = func(c *nf.Ctx) string {
|
||||
token := c.Get("Authorization")
|
||||
if token == "" {
|
||||
token = c.Cookies(opt.CookieName)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func NewAuth() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
token := tokenFunc(c)
|
||||
|
||||
if token = strings.TrimPrefix(token, "Bearer "); token == "" {
|
||||
return resp.Resp401(c, token)
|
||||
}
|
||||
|
||||
log.Debug(c.Context(), "middleware.NewAuth: token=%s", token)
|
||||
|
||||
target, err := controller.UserController.GetUserByToken(c.Context(), token)
|
||||
if err != nil {
|
||||
log.Error(c.Context(), "middleware.NewAuth: get user by token=%s err=%v", token, err)
|
||||
if errors.Is(err, cache.ErrorKeyNotFound) {
|
||||
return resp.Resp401(c, err)
|
||||
}
|
||||
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, err.Error(), err.Error())
|
||||
}
|
||||
|
||||
c.Locals("user", target)
|
||||
c.Locals("token", token)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
129
internal/middleware/cache/cache.go
vendored
Normal file
129
internal/middleware/cache/cache.go
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"esway/internal/database/cache"
|
||||
"esway/internal/model"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultKeyFn = func(c *nf.Ctx) string {
|
||||
return c.Request.URL.String()
|
||||
}
|
||||
defaultTimeout = 3600
|
||||
defaultPrefix = "midd:cache"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// if return "" (won't cache)
|
||||
KeyFn func(c *nf.Ctx) string
|
||||
|
||||
// cache timeout(seconds)
|
||||
Timeout int
|
||||
|
||||
Prefix string
|
||||
Refresh bool
|
||||
}
|
||||
|
||||
type store struct {
|
||||
Body []byte `json:"body"`
|
||||
Header http.Header `json:"header"`
|
||||
When int64 `json:"when"`
|
||||
}
|
||||
|
||||
func New(cfgs ...Config) nf.HandlerFunc {
|
||||
if cache.Client == nil {
|
||||
log.Panic("[middleware.cache] database cache client is nil")
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if len(cfgs) > 0 {
|
||||
cfg = cfgs[0]
|
||||
}
|
||||
|
||||
if cfg.KeyFn == nil {
|
||||
cfg.KeyFn = defaultKeyFn
|
||||
}
|
||||
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = defaultTimeout
|
||||
}
|
||||
|
||||
if cfg.Prefix == "" {
|
||||
cfg.Prefix = defaultPrefix
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
key string
|
||||
err error
|
||||
bs []byte
|
||||
res = new(store)
|
||||
)
|
||||
|
||||
if key = cfg.KeyFn(c); key == "" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
key = cfg.Prefix + ":" + key
|
||||
duration := time.Duration(cfg.Timeout) * time.Second
|
||||
|
||||
if cfg.Refresh {
|
||||
if bs, err = cache.Client.GetEx(c.Context(), key, duration); err != nil {
|
||||
if !errors.Is(err, cache.ErrorKeyNotFound) {
|
||||
log.Warn("[middleware.cache] cache get err: %s", err.Error())
|
||||
}
|
||||
goto FromNext
|
||||
}
|
||||
} else {
|
||||
if bs, err = cache.Client.Get(c.Context(), key); err != nil {
|
||||
if !errors.Is(err, cache.ErrorKeyNotFound) {
|
||||
log.Warn("[middleware.cache] cache get err: %s", err.Error())
|
||||
}
|
||||
goto FromNext
|
||||
}
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, res); err != nil {
|
||||
log.Warn("[middleware.cache] cache data unamrshal err: %s", err.Error())
|
||||
goto FromNext
|
||||
}
|
||||
|
||||
for key := range res.Header {
|
||||
for idx := range res.Header[key] {
|
||||
c.SetHeader(key, res.Header[key][idx])
|
||||
}
|
||||
}
|
||||
|
||||
c.SetHeader("X-Nf-Cache-At", strconv.Itoa(int(res.When)))
|
||||
|
||||
_, err = c.Write(res.Body)
|
||||
return err
|
||||
|
||||
FromNext:
|
||||
|
||||
blw := model.NewCopyWriter(c.Writer)
|
||||
c.Writer = blw
|
||||
|
||||
rerr := c.Next()
|
||||
|
||||
resp := blw.Bytes()
|
||||
|
||||
data := &store{Body: resp, Header: blw.Header().Clone(), When: time.Now().UnixMilli()}
|
||||
cbs, _ := json.Marshal(data)
|
||||
|
||||
if err = cache.Client.SetEx(c.Context(), key, cbs, duration); err != nil {
|
||||
log.Warn("[middleware.cache] cache client setex err: %s", err.Error())
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
}
|
71
internal/middleware/logger/logger.go
Normal file
71
internal/middleware/logger/logger.go
Normal file
@ -0,0 +1,71 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"esway/internal/opt"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
IgnoreFn func(c *nf.Ctx) bool
|
||||
}
|
||||
|
||||
var defaultConfig = Config{
|
||||
IgnoreFn: func(c *nf.Ctx) bool { return false },
|
||||
}
|
||||
|
||||
func New(configs ...Config) nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
now = time.Now()
|
||||
logFn func(msg string, data ...any)
|
||||
ip = c.IP()
|
||||
cfg Config
|
||||
)
|
||||
|
||||
if len(configs) > 0 {
|
||||
cfg = configs[0]
|
||||
}
|
||||
|
||||
if cfg.IgnoreFn == nil {
|
||||
cfg.IgnoreFn = defaultConfig.IgnoreFn
|
||||
}
|
||||
|
||||
traceId := c.Context().Value(nf.TraceKey)
|
||||
c.Locals(nf.TraceKey, traceId)
|
||||
|
||||
err := c.Next()
|
||||
|
||||
c.Writer.Header().Set(nf.TraceKey, fmt.Sprint(traceId))
|
||||
c.Writer.Header().Add("X-NF-Module", opt.Cfg.Name)
|
||||
|
||||
if cfg.IgnoreFn(c) {
|
||||
return err
|
||||
}
|
||||
|
||||
status, _ := strconv.Atoi(c.Writer.Header().Get(resp.RealStatusHeader))
|
||||
duration := time.Since(now)
|
||||
|
||||
msg := fmt.Sprintf("%s | %15s | %d[%3d] | %s | %6s | %s", traceId, ip, c.StatusCode, status, tool.HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
|
||||
|
||||
switch {
|
||||
case status >= 500:
|
||||
logFn = log.Error
|
||||
case status >= 400:
|
||||
logFn = log.Warn
|
||||
default:
|
||||
logFn = log.Info
|
||||
}
|
||||
|
||||
logFn(msg)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
118
internal/middleware/oplog/new.go
Normal file
118
internal/middleware/oplog/new.go
Normal file
@ -0,0 +1,118 @@
|
||||
package oplog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"esway/internal/database/db"
|
||||
"esway/internal/model"
|
||||
"esway/internal/opt"
|
||||
"esway/internal/sqlType"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
var (
|
||||
_once = &sync.Once{}
|
||||
lc = make(chan *model.OpLog, 1024)
|
||||
)
|
||||
|
||||
// NewOpLog
|
||||
//
|
||||
// * 记录操作日志的 中间件使用方法如下:
|
||||
//
|
||||
// app := nf.New()
|
||||
// app.Post("/login", oplog.NewOpLog(ctx), HandleLog)
|
||||
//
|
||||
// func HandleLog(c *nf.Ctx) error {
|
||||
// // 你的操作逻辑
|
||||
// c.Local(opt.OpLogLocalKey, &oplog.OpLog{})
|
||||
// // 剩下某些逻辑
|
||||
// // return xxx
|
||||
// }
|
||||
func NewOpLog(ctx context.Context) nf.HandlerFunc {
|
||||
_once.Do(func() {
|
||||
go func() {
|
||||
var (
|
||||
err error
|
||||
ticker = time.NewTicker(time.Duration(opt.OpLogWriteDurationSecond) * time.Second)
|
||||
list = make([]*model.OpLog, 0, 1024)
|
||||
|
||||
write = func() {
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err = db.Default.Session(tool.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Create(&list).
|
||||
Error; err != nil {
|
||||
log.Error("middleware.NewOpLog: write logs err=%v", err)
|
||||
}
|
||||
|
||||
list = list[:0]
|
||||
}
|
||||
)
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break Loop
|
||||
case <-ticker.C:
|
||||
write()
|
||||
case item, ok := <-lc:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
list = append(list, item)
|
||||
|
||||
if len(list) >= 100 {
|
||||
write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write()
|
||||
}()
|
||||
})
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
now := time.Now()
|
||||
|
||||
err := c.Next()
|
||||
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
|
||||
opv := c.Locals(opt.OpLogLocalKey)
|
||||
logItem, ok := opv.(*OpLog)
|
||||
if !ok {
|
||||
log.Warn("middleware.NewOpLog: %s - %s local '%s' to [*OpLog] invalid", c.Method(), c.Path(), opt.OpLogLocalKey)
|
||||
return err
|
||||
}
|
||||
|
||||
logItem.Content["time"] = now.UnixMilli()
|
||||
logItem.Content["user_id"] = op.Id
|
||||
logItem.Content["username"] = op.Username
|
||||
logItem.Content["created_at"] = now.UnixMilli()
|
||||
|
||||
select {
|
||||
case lc <- &model.OpLog{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
UserId: op.Id,
|
||||
Username: op.Username,
|
||||
Type: logItem.Type,
|
||||
Content: sqlType.NewJSONB(logItem.Content),
|
||||
}:
|
||||
case <-tool.Timeout(3).Done():
|
||||
log.Warn("middleware.NewOpLog: %s - %s log -> chan timeout[3s]", c.Method, c.Path())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
8
internal/middleware/oplog/oplog.go
Normal file
8
internal/middleware/oplog/oplog.go
Normal file
@ -0,0 +1,8 @@
|
||||
package oplog
|
||||
|
||||
import "esway/internal/model"
|
||||
|
||||
type OpLog struct {
|
||||
Type model.OpLogType
|
||||
Content map[string]any
|
||||
}
|
87
internal/middleware/privilege/privilege.go
Normal file
87
internal/middleware/privilege/privilege.go
Normal file
@ -0,0 +1,87 @@
|
||||
package privilege
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"esway/internal/model"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Relation int64
|
||||
|
||||
type vf func(user *model.User, ps ...model.Privilege) error
|
||||
|
||||
const (
|
||||
RelationAnd Relation = iota + 1
|
||||
RelationOr
|
||||
)
|
||||
|
||||
var (
|
||||
AndFunc vf = func(user *model.User, ps ...model.Privilege) error {
|
||||
pm := lo.SliceToMap(user.Privileges, func(item model.Privilege) (int64, struct{}) {
|
||||
return item.Value(), struct{}{}
|
||||
})
|
||||
|
||||
for _, p := range ps {
|
||||
if _, exist := pm[p.Value()]; !exist {
|
||||
return fmt.Errorf("缺少权限: %d, %s, %s", p.Value(), p.Code(), p.Label())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
OrFunc vf = func(user *model.User, ps ...model.Privilege) error {
|
||||
pm := lo.SliceToMap(user.Privileges, func(item model.Privilege) (int64, struct{}) {
|
||||
return item.Value(), struct{}{}
|
||||
})
|
||||
|
||||
for _, p := range ps {
|
||||
if _, exist := pm[p.Value()]; exist {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("缺少权限: %s", strings.Join(
|
||||
lo.Map(ps, func(item model.Privilege, index int) string {
|
||||
return item.Code()
|
||||
}),
|
||||
", ",
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
func Verify(relation Relation, privileges ...model.Privilege) nf.HandlerFunc {
|
||||
var _vf vf
|
||||
|
||||
switch relation {
|
||||
case RelationAnd:
|
||||
_vf = AndFunc
|
||||
case RelationOr:
|
||||
_vf = OrFunc
|
||||
default:
|
||||
log.Panic("middleware.Verify: unknown relation")
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
if len(privileges) == 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err := _vf(op, privileges...); err != nil {
|
||||
return resp.Resp403(c, err.Error())
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
9
internal/model/es.go
Normal file
9
internal/model/es.go
Normal file
@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type ESReqRes struct {
|
||||
ClientRequest bool // 是否是 es client 的请求
|
||||
Path string
|
||||
Method string
|
||||
Indixes []string
|
||||
Api ESApi
|
||||
}
|
89
internal/model/init.go
Normal file
89
internal/model/init.go
Normal file
@ -0,0 +1,89 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"esway/internal/opt"
|
||||
"esway/internal/sqlType"
|
||||
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Init(db *gorm.DB) error {
|
||||
var err error
|
||||
|
||||
if err = initModel(db); err != nil {
|
||||
return fmt.Errorf("model.MustInit: init models err=%v", err)
|
||||
}
|
||||
|
||||
log.Info("MustInitModels: auto_migrate privilege model success")
|
||||
|
||||
if err = initData(db); err != nil {
|
||||
return fmt.Errorf("model.MustInit: init datas err=%v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initModel(client *gorm.DB) error {
|
||||
if err := client.AutoMigrate(
|
||||
&User{},
|
||||
&OpLog{},
|
||||
&Token{},
|
||||
&TokenIndex{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("InitModels: auto_migrate user model success")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initData(client *gorm.DB) error {
|
||||
var err error
|
||||
|
||||
{
|
||||
count := 0
|
||||
|
||||
if err = client.Model(&User{}).Select("count(id)").Take(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count < len(initUsers) {
|
||||
log.Warn("mustInitDatas: user count = 0, start init...")
|
||||
for _, user := range initUsers {
|
||||
if err = client.Model(&User{}).Create(user).Error; err != nil {
|
||||
if !strings.Contains(err.Error(), "SQLSTATE 23505") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opt.Cfg.DB.Type == "postgresql" {
|
||||
if err = client.Exec(`SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("InitDatas: creat init users success")
|
||||
} else {
|
||||
ps := make(sqlType.NumSlice[Privilege], 0)
|
||||
for _, item := range Privilege(0).All() {
|
||||
ps = append(ps, item.(Privilege))
|
||||
}
|
||||
if err = client.Model(&User{}).Where("id = ?", initUsers[0].Id).
|
||||
Updates(map[string]any{
|
||||
"privileges": ps,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("initDatas: update init users success")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
17
internal/model/interface.go
Normal file
17
internal/model/interface.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
290
internal/model/oplog.go
Normal file
290
internal/model/oplog.go
Normal file
@ -0,0 +1,290 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"esway/internal/sqlType"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/html"
|
||||
)
|
||||
|
||||
var FuncMap = template.FuncMap{
|
||||
"time_format": func(mil any, format string) string {
|
||||
return time.UnixMilli(cast.ToInt64(mil)).Format(format)
|
||||
},
|
||||
}
|
||||
|
||||
var _ OpLogger = (*OpLogType)(nil)
|
||||
|
||||
type OpLogType uint64
|
||||
|
||||
const (
|
||||
OpLogTypeLogin OpLogType = iota + 1
|
||||
OpLogTypeLogout
|
||||
OpLogTypeCreateUser
|
||||
OpLogTypeUpdateUser
|
||||
OpLogTypeDeleteUser
|
||||
|
||||
// todo: 添加自己的操作日志 分类
|
||||
)
|
||||
|
||||
func (o OpLogType) Value() int64 {
|
||||
return int64(o)
|
||||
}
|
||||
|
||||
func (o OpLogType) Code() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return "login"
|
||||
case OpLogTypeLogout:
|
||||
return "logout"
|
||||
case OpLogTypeCreateUser:
|
||||
return "create_user"
|
||||
case OpLogTypeUpdateUser:
|
||||
return "update_user"
|
||||
case OpLogTypeDeleteUser:
|
||||
return "delete_user"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (o OpLogType) Label() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return "登入"
|
||||
case OpLogTypeLogout:
|
||||
return "登出"
|
||||
case OpLogTypeCreateUser:
|
||||
return "创建用户"
|
||||
case OpLogTypeUpdateUser:
|
||||
return "修改用户"
|
||||
case OpLogTypeDeleteUser:
|
||||
return "删除用户"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (o OpLogType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": o.Value(),
|
||||
"code": o.Code(),
|
||||
"label": o.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
func (o OpLogType) All() []Enum {
|
||||
return []Enum{
|
||||
OpLogTypeLogin,
|
||||
OpLogTypeLogout,
|
||||
OpLogTypeCreateUser,
|
||||
OpLogTypeUpdateUser,
|
||||
OpLogTypeDeleteUser,
|
||||
}
|
||||
}
|
||||
|
||||
func _trimHTML(v []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(v)
|
||||
}
|
||||
|
||||
var _mini = minify.New()
|
||||
|
||||
func init() {
|
||||
_mini.AddFunc("text/html", html.Minify)
|
||||
}
|
||||
|
||||
func (o OpLogType) Render(content map[string]any) (string, error) {
|
||||
var (
|
||||
err error
|
||||
render *template.Template
|
||||
buf bytes.Buffer
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if render, err = template.New(o.Code()).
|
||||
Funcs(FuncMap).
|
||||
Parse(o.Template()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = render.Execute(&buf, content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if bs, err = _mini.Bytes("text/html", buf.Bytes()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return _trimHTML(bs), nil
|
||||
}
|
||||
|
||||
const (
|
||||
oplogTemplateLogin = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登入
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateLogout = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登出
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateCreateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
创建
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateUpdateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user='{{ .user_id }}'
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time='{{ .time }}'
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
编辑
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateDeleteUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
删除
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
func (o OpLogType) Template() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return oplogTemplateLogin
|
||||
case OpLogTypeLogout:
|
||||
return oplogTemplateLogout
|
||||
case OpLogTypeCreateUser:
|
||||
return oplogTemplateCreateUser
|
||||
case OpLogTypeUpdateUser:
|
||||
return oplogTemplateUpdateUser
|
||||
case OpLogTypeDeleteUser:
|
||||
return oplogTemplateDeleteUser
|
||||
default:
|
||||
return `<div>错误的日志类型</div>`
|
||||
}
|
||||
}
|
||||
|
||||
type OpLog struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
UserId uint64 `json:"user_id" gorm:"column:user_id"`
|
||||
Username string `json:"username" gorm:"column:username;varchar(128)"`
|
||||
Type OpLogType `json:"type" gorm:"column:type"`
|
||||
Content sqlType.JSONB `json:"content" gorm:"column:content;type:jsonb"`
|
||||
HTML string `json:"html" gorm:"-"`
|
||||
}
|
61
internal/model/privilege.go
Normal file
61
internal/model/privilege.go
Normal file
@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Privilege uint64
|
||||
|
||||
type _privilege struct {
|
||||
Value int64 `json:"value"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
const (
|
||||
PrivilegeUserManage Privilege = iota + 1
|
||||
PrivilegeOpLog
|
||||
)
|
||||
|
||||
func (p Privilege) Value() int64 {
|
||||
return int64(p)
|
||||
}
|
||||
|
||||
func (p Privilege) Code() string {
|
||||
switch p {
|
||||
case PrivilegeUserManage:
|
||||
return "user_manage"
|
||||
case PrivilegeOpLog:
|
||||
return "oplog"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (p Privilege) Label() string {
|
||||
switch p {
|
||||
case PrivilegeUserManage:
|
||||
return "用户管理"
|
||||
case PrivilegeOpLog:
|
||||
return "操作日志"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (p Privilege) MarshalJSON() ([]byte, error) {
|
||||
_p := &_privilege{
|
||||
Value: int64(p),
|
||||
Code: p.Code(),
|
||||
Label: p.Label(),
|
||||
}
|
||||
|
||||
return json.Marshal(_p)
|
||||
}
|
||||
|
||||
func (p Privilege) All() []Enum {
|
||||
return []Enum{
|
||||
PrivilegeUserManage,
|
||||
PrivilegeOpLog,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Enum = (*Privilege)(nil)
|
87
internal/model/role.go
Normal file
87
internal/model/role.go
Normal file
@ -0,0 +1,87 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"esway/internal/opt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type _role struct {
|
||||
Value uint8 `json:"value"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Role uint8
|
||||
|
||||
var _ Enum = Role(0)
|
||||
|
||||
func (u Role) MarshalJSON() ([]byte, error) {
|
||||
m := _role{
|
||||
Value: uint8(u),
|
||||
Code: u.Code(),
|
||||
Label: u.Label(),
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
const (
|
||||
RoleRoot Role = 255
|
||||
RoleAdmin Role = 254
|
||||
RoleUser Role = 100
|
||||
)
|
||||
|
||||
func (u Role) Code() string {
|
||||
switch u {
|
||||
case RoleRoot:
|
||||
return "root"
|
||||
case RoleAdmin:
|
||||
return "admin"
|
||||
case RoleUser:
|
||||
return "user"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Label() string {
|
||||
switch u {
|
||||
case RoleRoot:
|
||||
return "根用户"
|
||||
case RoleAdmin:
|
||||
return "管理员"
|
||||
case RoleUser:
|
||||
return "用户"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Value() int64 {
|
||||
return int64(u)
|
||||
}
|
||||
|
||||
func (u Role) All() []Enum {
|
||||
return []Enum{
|
||||
RoleAdmin,
|
||||
RoleUser,
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Where(db *gorm.DB) *gorm.DB {
|
||||
if opt.RoleMustLess {
|
||||
return db.Where("users.role < ?", u.Value())
|
||||
} else {
|
||||
return db.Where("users.role <= ?", u.Value())
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) CanOP(op *User) bool {
|
||||
if opt.RoleMustLess {
|
||||
return op.Role > u
|
||||
}
|
||||
|
||||
return op.Role >= u
|
||||
}
|
36
internal/model/token.go
Normal file
36
internal/model/token.go
Normal file
@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import "esway/internal/sqlType"
|
||||
|
||||
type Token struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
CreatedBy uint64
|
||||
Token string
|
||||
Comment string
|
||||
}
|
||||
|
||||
type TokenIndex struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
TokenId uint64
|
||||
Index string
|
||||
APIs sqlType.StrSlice
|
||||
}
|
||||
|
||||
type ESApi string
|
||||
|
||||
const (
|
||||
ESApiUnknown ESApi = "unknown"
|
||||
ESApiSearch ESApi = "search"
|
||||
ESApiGetDoc ESApi = "doc"
|
||||
ESApiUpdateDoc ESApi = "update"
|
||||
ESApiUpdateByQuery ESApi = "update_by_query"
|
||||
ESApiReindex ESApi = "reindex"
|
||||
)
|
227
internal/model/user.go
Normal file
227
internal/model/user.go
Normal file
@ -0,0 +1,227 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"esway/internal/opt"
|
||||
"esway/internal/sqlType"
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
var (
|
||||
initUsers = []*User{
|
||||
{
|
||||
Id: 1,
|
||||
Username: "admin",
|
||||
Password: tool.NewPassword("123456"),
|
||||
Nickname: "admin",
|
||||
Role: RoleAdmin,
|
||||
Privileges: lo.Map(Privilege(0).All(), func(item Enum, index int) Privilege {
|
||||
return item.(Privilege)
|
||||
}),
|
||||
CreatedById: 1,
|
||||
CreatedByName: "admin",
|
||||
ActiveAt: time.Now().UnixMilli(),
|
||||
Deadline: time.Now().AddDate(100, 0, 0).UnixMilli(),
|
||||
},
|
||||
}
|
||||
|
||||
_ Enum = Status(0)
|
||||
)
|
||||
|
||||
type Status uint64
|
||||
|
||||
const (
|
||||
StatusNormal Status = iota
|
||||
StatusFrozen
|
||||
)
|
||||
|
||||
func (s Status) Value() int64 {
|
||||
return int64(s)
|
||||
}
|
||||
|
||||
func (s Status) Code() string {
|
||||
switch s {
|
||||
case StatusNormal:
|
||||
return "normal"
|
||||
case StatusFrozen:
|
||||
return "frozen"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) Label() string {
|
||||
switch s {
|
||||
case StatusNormal:
|
||||
return "正常"
|
||||
case StatusFrozen:
|
||||
return "冻结"
|
||||
default:
|
||||
return "异常"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) All() []Enum {
|
||||
return []Enum{
|
||||
StatusNormal,
|
||||
StatusFrozen,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": s.Value(),
|
||||
"code": s.Code(),
|
||||
"label": s.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
Username string `json:"username" gorm:"column:username;type:varchar(64);unique"`
|
||||
Password string `json:"-" gorm:"column:password;type:varchar(256)"`
|
||||
|
||||
Status Status `json:"status" gorm:"column:status;default:0"`
|
||||
|
||||
Nickname string `json:"nickname" gorm:"column:nickname;type:varchar(64)"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
|
||||
Role Role `json:"role" gorm:"column:role"`
|
||||
Privileges sqlType.NumSlice[Privilege] `json:"privileges" gorm:"column:privileges;type:bigint[]"`
|
||||
|
||||
CreatedById uint64 `json:"created_by_id" gorm:"column:created_by_id"`
|
||||
CreatedByName string `json:"created_by_name" gorm:"column:created_by_name;type:varchar(64)"`
|
||||
|
||||
ActiveAt int64 `json:"active_at" gorm:"column:active_at"`
|
||||
Deadline int64 `json:"deadline" gorm:"column:deadline"`
|
||||
|
||||
LoginAt int64 `json:"login_at" gorm:"-"`
|
||||
}
|
||||
|
||||
func (u *User) CheckStatus(mustOk bool) error {
|
||||
switch u.Status {
|
||||
case StatusNormal:
|
||||
case StatusFrozen:
|
||||
if mustOk {
|
||||
return errors.New("用户被冻结")
|
||||
}
|
||||
default:
|
||||
return errors.New("用户状态未知")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) IsValid(mustOk bool) error {
|
||||
now := time.Now()
|
||||
|
||||
if now.UnixMilli() >= u.Deadline {
|
||||
return errors.New("用户已过期")
|
||||
}
|
||||
|
||||
if now.UnixMilli() < u.ActiveAt {
|
||||
return errors.New("用户未启用")
|
||||
}
|
||||
|
||||
if u.DeletedAt > 0 {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
return u.CheckStatus(mustOk)
|
||||
}
|
||||
|
||||
func (u *User) JwtEncode() (token string, err error) {
|
||||
now := time.Now()
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
||||
"id": u.Id,
|
||||
"username": u.Username,
|
||||
"status": u.Status,
|
||||
"deadline": u.Deadline,
|
||||
"login_at": now.UnixMilli(),
|
||||
})
|
||||
|
||||
if token, err = jwtToken.SignedString([]byte(opt.JwtTokenSecret)); err != nil {
|
||||
err = fmt.Errorf("JwtEncode: jwt token signed secret err: %v", err)
|
||||
log.Error(err.Error())
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u *User) FromJwt(token string) *User {
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
pt *jwt.Token
|
||||
claims jwt.MapClaims
|
||||
)
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
if pt, err = jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok = t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(opt.JwtTokenSecret), nil
|
||||
}); err != nil {
|
||||
log.Error("jwt parse err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !pt.Valid {
|
||||
log.Warn("parsed jwt invalid")
|
||||
return nil
|
||||
}
|
||||
|
||||
if claims, ok = pt.Claims.(jwt.MapClaims); !ok {
|
||||
log.Error("convert jwt claims err")
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Id = cast.ToUint64(claims["user_id"])
|
||||
u.Username = cast.ToString(claims["username"])
|
||||
u.Status = Status(cast.ToInt64(claims["status"]))
|
||||
u.Deadline = cast.ToInt64(claims["deadline"])
|
||||
u.LoginAt = cast.ToInt64(claims["login_at"])
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u User) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"id": u.Id,
|
||||
"created_at": u.CreatedAt,
|
||||
"updated_at": u.UpdatedAt,
|
||||
"deleted_at": u.DeletedAt,
|
||||
"username": u.Username,
|
||||
"status": u.Status.Value(),
|
||||
"nickname": u.Nickname,
|
||||
"comment": u.Comment,
|
||||
"role": uint8(u.Role),
|
||||
"privileges": lo.Map(u.Privileges, func(item Privilege, index int) int64 {
|
||||
return item.Value()
|
||||
}),
|
||||
"created_by_id": u.CreatedById,
|
||||
"created_by_name": u.CreatedByName,
|
||||
"active_at": u.ActiveAt,
|
||||
"deadline": u.Deadline,
|
||||
"login_at": u.LoginAt,
|
||||
})
|
||||
}
|
34
internal/model/writer.go
Normal file
34
internal/model/writer.go
Normal file
@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
)
|
||||
|
||||
type CopyResponseWriter struct {
|
||||
nf.ResponseWriter
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w CopyResponseWriter) Write(b []byte) (int, error) {
|
||||
w.buf.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w CopyResponseWriter) String() string {
|
||||
return w.buf.String()
|
||||
}
|
||||
|
||||
func (w CopyResponseWriter) Bytes() []byte {
|
||||
return w.buf.Bytes()
|
||||
}
|
||||
|
||||
func (w CopyResponseWriter) Reader() io.Reader {
|
||||
return w.buf
|
||||
}
|
||||
|
||||
func NewCopyWriter(w nf.ResponseWriter) *CopyResponseWriter {
|
||||
return &CopyResponseWriter{buf: bytes.NewBuffer(make([]byte, 0, 1024)), ResponseWriter: w}
|
||||
}
|
62
internal/opt/opt.go
Normal file
62
internal/opt/opt.go
Normal file
@ -0,0 +1,62 @@
|
||||
package opt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"esway/internal/tool"
|
||||
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
type listen struct {
|
||||
Gateway string `json:"gateway"`
|
||||
Dashboard string `json:"dashboard"`
|
||||
}
|
||||
|
||||
type db struct {
|
||||
Type string `json:"-"` // postgres, mysql, sqlite
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Name string `json:"name"`
|
||||
Debug bool `json:"-"`
|
||||
Dev bool `json:"-"`
|
||||
Config string `json:"-"`
|
||||
Listen listen `json:"listen"`
|
||||
Endpoints []string `json:"endpoints"`
|
||||
DB db `json:"db"`
|
||||
Cache cache `json:"cache"`
|
||||
}
|
||||
|
||||
var (
|
||||
Mode string
|
||||
Cfg = &config{}
|
||||
)
|
||||
|
||||
func Init(filename string) error {
|
||||
var (
|
||||
err error
|
||||
bs []byte
|
||||
)
|
||||
|
||||
log.Info("opt.Init: start reading config file: %s", filename)
|
||||
|
||||
if bs, err = os.ReadFile(filename); err != nil {
|
||||
return fmt.Errorf("opt.Init: read config file=%s err=%v", filename, err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, Cfg); err != nil {
|
||||
return fmt.Errorf("opt.Init: json marshal config=%s err=%v", string(bs), err)
|
||||
}
|
||||
|
||||
tool.TablePrinter(Cfg)
|
||||
|
||||
return nil
|
||||
}
|
55
internal/opt/var.go
Normal file
55
internal/opt/var.go
Normal file
@ -0,0 +1,55 @@
|
||||
package opt
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
RpcAddress = "tcp://127.0.0.1:8081" // unix:///tmp/xxx.sock
|
||||
// todo: 可以替换自己生生成的 secret
|
||||
JwtTokenSecret = "7^D+UW3BPB2Mnz)bY3uVrAUyv&dj8Kdz"
|
||||
|
||||
// todo: 是否打开 gorm 的 debug 打印 (开发和 dev 环境时可以打开)
|
||||
DBDebug = true
|
||||
|
||||
// todo: 是否加载默认的前端用户管理界面
|
||||
EnableFront = false
|
||||
|
||||
// todo: 同一个账号是否可以多 client 登录
|
||||
MultiLogin = false
|
||||
|
||||
// todo: 用户量不大的情况, 并没有缓存用户具体信息, 如果需要可以打开
|
||||
EnableUserCache = true
|
||||
|
||||
// todo: 缓存时, key 的前缀
|
||||
CachePrefix = "esway"
|
||||
|
||||
// todo: 登录颁发的 cookie 的 name
|
||||
CookieName = "utlone-token"
|
||||
|
||||
// todo: 用户列表,日志列表 size 参数
|
||||
DefaultSize, MaxSize = 20, 200
|
||||
|
||||
// todo: 操作用户时, role 相等时能否操作: 包括 列表, 能否新建,修改,删除同样 role 的用户
|
||||
RoleMustLess = false
|
||||
|
||||
// todo: 通过 c.Local() 存入 oplog 时的 key 值
|
||||
OpLogLocalKey = "oplog"
|
||||
|
||||
// todo: 操作日志 最多延迟多少秒写入(最多缓存多少秒的日志,然后 bulk 写入)
|
||||
OpLogWriteDurationSecond = 5
|
||||
|
||||
LocalTraceKey = "X-Trace-Id"
|
||||
|
||||
LoginURL = "/login"
|
||||
)
|
||||
|
||||
var (
|
||||
Locker = &sync.Mutex{}
|
||||
// todo: 颁发的 token, (cookie) 在缓存中存在的时间 (每次请求该时间也会被刷新)
|
||||
TokenTimeout = time.Duration(3600*12) * time.Second
|
||||
|
||||
Start = time.Now()
|
||||
OK bool
|
||||
)
|
9
internal/sqlType/err.go
Normal file
9
internal/sqlType/err.go
Normal file
@ -0,0 +1,9 @@
|
||||
package sqlType
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrConvertScanVal = errors.New("convert scan val to str err")
|
||||
ErrInvalidScanVal = errors.New("scan val invalid")
|
||||
ErrConvertVal = errors.New("convert err")
|
||||
)
|
76
internal/sqlType/jsonb.go
Normal file
76
internal/sqlType/jsonb.go
Normal file
@ -0,0 +1,76 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
type JSONB struct {
|
||||
Val pgtype.JSONB
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func NewJSONB(v interface{}) JSONB {
|
||||
j := new(JSONB)
|
||||
j.Val = pgtype.JSONB{}
|
||||
if err := j.Val.Set(v); err == nil {
|
||||
j.Valid = true
|
||||
return *j
|
||||
}
|
||||
|
||||
return *j
|
||||
}
|
||||
|
||||
func (j *JSONB) Set(value interface{}) error {
|
||||
if err := j.Val.Set(value); err != nil {
|
||||
j.Valid = false
|
||||
return err
|
||||
}
|
||||
|
||||
j.Valid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JSONB) Bind(model interface{}) error {
|
||||
return j.Val.AssignTo(model)
|
||||
}
|
||||
|
||||
func (j *JSONB) Scan(value interface{}) error {
|
||||
j.Val = pgtype.JSONB{}
|
||||
if value == nil {
|
||||
j.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
j.Valid = true
|
||||
|
||||
return j.Val.Scan(value)
|
||||
}
|
||||
|
||||
func (j JSONB) Value() (driver.Value, error) {
|
||||
if j.Valid {
|
||||
return j.Val.Value()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (j JSONB) MarshalJSON() ([]byte, error) {
|
||||
if j.Valid {
|
||||
return j.Val.MarshalJSON()
|
||||
}
|
||||
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
func (j *JSONB) UnmarshalJSON(b []byte) error {
|
||||
if string(b) == "null" {
|
||||
j.Valid = false
|
||||
return j.Val.UnmarshalJSON(b)
|
||||
}
|
||||
|
||||
return j.Val.UnmarshalJSON(b)
|
||||
}
|
42
internal/sqlType/nullStr.go
Normal file
42
internal/sqlType/nullStr.go
Normal file
@ -0,0 +1,42 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// type NullString struct {
|
||||
// sql.NullString
|
||||
// }
|
||||
|
||||
type NullString struct{ sql.NullString }
|
||||
|
||||
func NewNullString(val string) NullString {
|
||||
if val == "" {
|
||||
return NullString{}
|
||||
}
|
||||
|
||||
return NullString{sql.NullString{Valid: true, String: val}}
|
||||
}
|
||||
|
||||
func (ns NullString) MarshalJSON() ([]byte, error) {
|
||||
if !ns.Valid {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
return json.Marshal(ns.String)
|
||||
}
|
||||
|
||||
func (ns *NullString) UnmarshalJSON(data []byte) error {
|
||||
if string(data) == "null" {
|
||||
ns.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &ns.String); err != nil {
|
||||
ns.Valid = true
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
53
internal/sqlType/set.go
Normal file
53
internal/sqlType/set.go
Normal file
@ -0,0 +1,53 @@
|
||||
package sqlType
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Set map[string]struct{}
|
||||
|
||||
func (s Set) MarshalJSON() ([]byte, error) {
|
||||
array := make([]string, 0)
|
||||
for name := range s {
|
||||
array = append(array, name)
|
||||
}
|
||||
|
||||
return json.Marshal(array)
|
||||
}
|
||||
|
||||
func (s *Set) UnmarshalJSON(b []byte) error {
|
||||
array := make([]string, 0)
|
||||
if err := json.Unmarshal(b, &array); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set := make(map[string]struct{})
|
||||
|
||||
for _, name := range array {
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
|
||||
*s = set
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Set) ToStringSlice() []string {
|
||||
var (
|
||||
result = make([]string, 0, len(s))
|
||||
)
|
||||
|
||||
for key := range s {
|
||||
result = append(result, key)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Set) FromStringSlice(ss *[]string) {
|
||||
if s == nil {
|
||||
m := make(Set)
|
||||
s = &m
|
||||
}
|
||||
|
||||
for idx := range *(ss) {
|
||||
(*s)[(*ss)[idx]] = struct{}{}
|
||||
}
|
||||
}
|
109
internal/sqlType/strSlice.go
Normal file
109
internal/sqlType/strSlice.go
Normal file
@ -0,0 +1,109 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type StrSlice []string
|
||||
|
||||
func (s *StrSlice) Scan(val interface{}) error {
|
||||
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrConvertScanVal
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs := make([]byte, 0, 128)
|
||||
bss := make([]byte, 0, 2*len(str))
|
||||
|
||||
quoteCount := 0
|
||||
|
||||
for idx := 1; idx < len(str)-1; idx++ {
|
||||
// 44: , 92: \ 34: "
|
||||
quote := str[idx]
|
||||
switch quote {
|
||||
case 44:
|
||||
if quote == 44 && str[idx-1] != 92 && quoteCount == 0 {
|
||||
if len(bs) > 0 {
|
||||
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
|
||||
bs = append([]byte{34}, bs...)
|
||||
bs = append(bs, 34)
|
||||
}
|
||||
|
||||
bss = append(bss, bs...)
|
||||
bss = append(bss, 44)
|
||||
}
|
||||
bs = bs[:0]
|
||||
} else {
|
||||
bs = append(bs, quote)
|
||||
}
|
||||
case 34:
|
||||
if str[idx-1] != 92 {
|
||||
quoteCount = (quoteCount + 1) % 2
|
||||
}
|
||||
bs = append(bs, quote)
|
||||
default:
|
||||
bs = append(bs, quote)
|
||||
}
|
||||
|
||||
//bs = append(bs, str[idx])
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
|
||||
bs = append([]byte{34}, bs...)
|
||||
bs = append(bs, 34)
|
||||
}
|
||||
|
||||
bss = append(bss, bs...)
|
||||
} else {
|
||||
if len(bss) > 2 {
|
||||
bss = bss[:len(bss)-2]
|
||||
}
|
||||
}
|
||||
|
||||
bss = append([]byte{'['}, append(bss, ']')...)
|
||||
|
||||
if err := json.Unmarshal(bss, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s StrSlice) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
encoder := json.NewEncoder(buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
if err := encoder.Encode(s); err != nil {
|
||||
return "{}", err
|
||||
}
|
||||
|
||||
bs := buf.Bytes()
|
||||
|
||||
bs[0] = '{'
|
||||
|
||||
if bs[len(bs)-1] == 10 {
|
||||
bs = bs[:len(bs)-1]
|
||||
}
|
||||
|
||||
bs[len(bs)-1] = '}'
|
||||
|
||||
return string(bs), nil
|
||||
}
|
71
internal/sqlType/uint64Slice.go
Normal file
71
internal/sqlType/uint64Slice.go
Normal file
@ -0,0 +1,71 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type NumSlice[T ~int | ~int64 | ~uint | ~uint64] []T
|
||||
|
||||
func (n *NumSlice[T]) Scan(val interface{}) error {
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrConvertScanVal
|
||||
}
|
||||
|
||||
length := len(str)
|
||||
|
||||
if length <= 0 {
|
||||
*n = make(NumSlice[T], 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
if str[0] != '{' || str[length-1] != '}' {
|
||||
return ErrInvalidScanVal
|
||||
}
|
||||
|
||||
str = str[1 : length-1]
|
||||
if len(str) == 0 {
|
||||
*n = make(NumSlice[T], 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
numStrs := strings.Split(str, ",")
|
||||
nums := make([]T, len(numStrs))
|
||||
|
||||
for idx := range numStrs {
|
||||
num, err := cast.ToInt64E(strings.TrimSpace(numStrs[idx]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: can't convert to %T", ErrConvertVal, T(0))
|
||||
}
|
||||
|
||||
nums[idx] = T(num)
|
||||
}
|
||||
|
||||
*n = nums
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NumSlice[T]) Value() (driver.Value, error) {
|
||||
if n == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
if len(n) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
ss := make([]string, 0, len(n))
|
||||
for idx := range n {
|
||||
ss = append(ss, strconv.Itoa(int(n[idx])))
|
||||
}
|
||||
|
||||
s := strings.Join(ss, ", ")
|
||||
|
||||
return fmt.Sprintf("{%s}", s), nil
|
||||
}
|
38
internal/tool/ctx.go
Normal file
38
internal/tool/ctx.go
Normal file
@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Timeout(seconds ...int) (ctx context.Context) {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
nctx, _ := context.WithTimeout(ctx, duration)
|
||||
|
||||
return nctx
|
||||
}
|
24
internal/tool/human.go
Normal file
24
internal/tool/human.go
Normal file
@ -0,0 +1,24 @@
|
||||
package tool
|
||||
|
||||
import "fmt"
|
||||
|
||||
func HumanDuration(nano int64) string {
|
||||
duration := float64(nano)
|
||||
unit := "ns"
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "us"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "ms"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = " s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||
}
|
11
internal/tool/must.go
Normal file
11
internal/tool/must.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "github.com/loveuer/nf/nft/log"
|
||||
|
||||
func Must(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
84
internal/tool/password.go
Normal file
84
internal/tool/password.go
Normal file
@ -0,0 +1,84 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
EncryptHeader string = "pbkdf2:sha256" // 用户密码加密
|
||||
)
|
||||
|
||||
func NewPassword(password string) string {
|
||||
return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000))
|
||||
}
|
||||
|
||||
func ComparePassword(in, db string) bool {
|
||||
strs := strings.Split(db, "$")
|
||||
if len(strs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encs := strings.Split(strs[0], ":")
|
||||
if len(encs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encIteration, err := strconv.Atoi(encs[2])
|
||||
if err != nil {
|
||||
log.Error("password in db invalid: %s, convert iter err: %s", db, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return EncryptPassword(in, strs[1], encIteration) == db
|
||||
}
|
||||
|
||||
func EncryptPassword(password, salt string, iter int) string {
|
||||
hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
|
||||
encrypted := hex.EncodeToString(hash)
|
||||
return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted)
|
||||
}
|
||||
|
||||
func CheckPassword(password string) error {
|
||||
if len(password) < 8 || len(password) > 32 {
|
||||
return errors.New("密码长度不符合")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
match bool
|
||||
patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`}
|
||||
matchAccount = 0
|
||||
tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"}
|
||||
locktips = make([]string, 0)
|
||||
)
|
||||
|
||||
for idx, pattern := range patternList {
|
||||
match, err = regexp.MatchString(pattern, password)
|
||||
if err != nil {
|
||||
log.Warn("regex match string err, reg_str: %s, err: %v", pattern, err)
|
||||
return errors.New("密码强度不够")
|
||||
}
|
||||
|
||||
if match {
|
||||
matchAccount++
|
||||
} else {
|
||||
locktips = append(locktips, tips[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if matchAccount < 3 {
|
||||
return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
internal/tool/password_test.go
Normal file
11
internal/tool/password_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
|
||||
result := EncryptPassword(password, RandomString(8), 50000)
|
||||
|
||||
t.Logf("sum => %s", result)
|
||||
}
|
54
internal/tool/random.go
Normal file
54
internal/tool/random.go
Normal file
@ -0,0 +1,54 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterNum = []byte("0123456789")
|
||||
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterSyb = []byte("!@#$%^&*()_+-=")
|
||||
)
|
||||
|
||||
func RandomInt(max int64) int64 {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(max))
|
||||
return num.Int64()
|
||||
}
|
||||
|
||||
func RandomString(length int) string {
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
result[i] = letters[num.Int64()]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func RandomPassword(length int, withSymbol bool) string {
|
||||
result := make([]byte, length)
|
||||
kind := 3
|
||||
if withSymbol {
|
||||
kind++
|
||||
}
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
switch i % kind {
|
||||
case 0:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum))))
|
||||
result[i] = letterNum[num.Int64()]
|
||||
case 1:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow))))
|
||||
result[i] = letterLow[num.Int64()]
|
||||
case 2:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap))))
|
||||
result[i] = letterCap[num.Int64()]
|
||||
case 3:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb))))
|
||||
result[i] = letterSyb[num.Int64()]
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
124
internal/tool/table.go
Normal file
124
internal/tool/table.go
Normal file
@ -0,0 +1,124 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TablePrinter(data any, writers ...io.Writer) {
|
||||
var w io.Writer = os.Stdout
|
||||
if len(writers) > 0 && writers[0] != nil {
|
||||
w = writers[0]
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
structPrinter(t, "", data)
|
||||
_, _ = fmt.Fprintln(w, t.Render())
|
||||
}
|
||||
|
||||
func structPrinter(w table.Writer, prefix string, item any) {
|
||||
Start:
|
||||
rv := reflect.ValueOf(item)
|
||||
if rv.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
for rv.Type().Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Invalid,
|
||||
reflect.Uintptr,
|
||||
reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.UnsafePointer:
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.Complex64,
|
||||
reflect.Complex128,
|
||||
reflect.Interface:
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
|
||||
case reflect.String:
|
||||
val := rv.String()
|
||||
if len(val) <= 160 {
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val})
|
||||
return
|
||||
}
|
||||
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]})
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".")
|
||||
structPrinter(w, p, rv.Index(i).Interface())
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Pointer:
|
||||
goto Start
|
||||
case reflect.Struct:
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name)
|
||||
field := rv.Field(i)
|
||||
|
||||
//log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i))
|
||||
|
||||
if !field.CanInterface() {
|
||||
return
|
||||
}
|
||||
|
||||
structPrinter(w, p, field.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TableMapPrinter(data []byte) {
|
||||
m := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
log.Warn(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
addRow(t, "", m)
|
||||
fmt.Println(t.Render())
|
||||
}
|
||||
|
||||
func addRow(w table.Writer, prefix string, m any) {
|
||||
rv := reflect.ValueOf(m)
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
key := k.String()
|
||||
if prefix != "" {
|
||||
key = strings.Join([]string{prefix, k.String()}, ".")
|
||||
}
|
||||
addRow(w, key, rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface())
|
||||
}
|
||||
default:
|
||||
w.AppendRow(table.Row{prefix, m})
|
||||
}
|
||||
}
|
19
internal/tool/tools.go
Normal file
19
internal/tool/tools.go
Normal file
@ -0,0 +1,19 @@
|
||||
package tool
|
||||
|
||||
import "cmp"
|
||||
|
||||
func Min[T cmp.Ordered](a, b T) T {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func Max[T cmp.Ordered](a, b T) T {
|
||||
if a >= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
59
internal/unix/handler.go
Normal file
59
internal/unix/handler.go
Normal file
@ -0,0 +1,59 @@
|
||||
package unix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"esway/internal/log"
|
||||
"esway/internal/opt"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
type (
|
||||
AvailableReq struct{}
|
||||
AvailableResp struct {
|
||||
OK bool
|
||||
Now time.Time
|
||||
Start time.Time
|
||||
Duration string
|
||||
}
|
||||
)
|
||||
|
||||
func (*Handler) Available(_ *AvailableReq, out *AvailableResp) error {
|
||||
now := time.Now()
|
||||
out.OK, out.Now = opt.OK, now
|
||||
out.Start = opt.Start
|
||||
out.Duration = fmt.Sprint(now.Sub(opt.Start))
|
||||
return nil
|
||||
}
|
||||
|
||||
type SettingReq struct {
|
||||
Debug bool
|
||||
}
|
||||
type Resp[T any] struct {
|
||||
Status uint32
|
||||
Msg string
|
||||
Data T
|
||||
}
|
||||
|
||||
func (h *Handler) Setting(in *SettingReq, out *Resp[bool]) error {
|
||||
opt.Locker.Lock()
|
||||
defer opt.Locker.Unlock()
|
||||
|
||||
if in.Debug {
|
||||
opt.Cfg.Debug = true
|
||||
log.Info(h.Ctx, "set global debug[true]")
|
||||
} else {
|
||||
opt.Cfg.Debug = false
|
||||
log.Info(h.Ctx, "set global debug[false]")
|
||||
}
|
||||
|
||||
out.Status = 200
|
||||
out.Msg = "操作成功"
|
||||
|
||||
return nil
|
||||
}
|
52
internal/unix/start.go
Normal file
52
internal/unix/start.go
Normal file
@ -0,0 +1,52 @@
|
||||
package unix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/url"
|
||||
|
||||
"esway/internal/log"
|
||||
"esway/internal/opt"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
ready := make(chan bool)
|
||||
defer close(ready)
|
||||
|
||||
uri, err := url.Parse(opt.RpcAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
address := uri.Host + uri.Path
|
||||
log.Debug(ctx, "[rpc-svc] listen at [%s] [%s]", uri.Scheme, address)
|
||||
|
||||
ln, err := net.Listen(uri.Scheme, address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
svc := rpc.NewServer()
|
||||
if err = svc.RegisterName("svc", &Handler{Ctx: ctx}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Info(ctx, "[rpc-svc] start at: [%s] [%s]", uri.Scheme, address)
|
||||
ready <- true
|
||||
svc.Accept(ln)
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
return nil
|
||||
}
|
20
main.go
Normal file
20
main.go
Normal file
@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"esway/internal/cmd"
|
||||
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
|
||||
if err := cmd.Execute(ctx); err != nil {
|
||||
log.Error("cmd.Execute: err=%v", err)
|
||||
}
|
||||
}
|
28
readme.md
Normal file
28
readme.md
Normal file
@ -0,0 +1,28 @@
|
||||
# esway: elasticsearch gateway
|
||||
|
||||
### Run
|
||||
- `go run . --help`
|
||||
- `go run . --debug`
|
||||
- `go run . -c etc/config.json`
|
||||
|
||||
### Build
|
||||
- `docker build -t {repo:tag} -f Dockerfile .`
|
||||
|
||||
### Development
|
||||
- `准备 es`
|
||||
- `准备 esway 并且假设监听在 8200`
|
||||
- `准备 kibana 如下:`
|
||||
|
||||
```yaml
|
||||
|
||||
version: "3"
|
||||
kibana-proxy:
|
||||
image: kibana:7.17.1
|
||||
container_name: kibana-proxy
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ELASTICSEARCH_HOSTS: http://<esway_ip>:<esway_port(8200)>
|
||||
I18N_LOCALE: zh-CN
|
||||
network_mode: "host"
|
||||
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user