wip: 完成 client api 分析
This commit is contained in:
commit
64cdd0cb0e
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