From 64cdd0cb0ea76c88a04a006b495ff4b8b400e0c7 Mon Sep 17 00:00:00 2001 From: loveuer Date: Thu, 19 Dec 2024 15:03:36 +0800 Subject: [PATCH] =?UTF-8?q?wip:=20=E5=AE=8C=E6=88=90=20client=20api=20?= =?UTF-8?q?=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 + .nfctl | 35 ++ Dockerfile | 30 ++ deployment/database.dev.yml | 56 ++ deployment/run.sh | 8 + etc/config.json | 19 + go.mod | 70 +++ go.sum | 392 ++++++++++++++ httptest/user.http | 29 + internal/api/api.go | 70 +++ internal/api/start.go | 45 ++ internal/cmd/cli.go | 78 +++ internal/cmd/execute.go | 37 ++ internal/cmd/init.go | 13 + internal/cmd/root.go | 26 + internal/controller/impl.go | 14 + internal/controller/user.go | 164 ++++++ internal/database/cache/cache_lru.go | 111 ++++ internal/database/cache/cache_memory.go | 75 +++ internal/database/cache/cache_redis.go | 63 +++ internal/database/cache/client.go | 37 ++ internal/database/cache/error.go | 7 + internal/database/cache/init.go | 69 +++ internal/database/db/client.go | 52 ++ internal/database/db/db_test.go | 8 + internal/database/db/new.go | 55 ++ internal/database/es/client.go | 39 ++ internal/database/es/raw.go | 1 + internal/gateway/gateway.go | 63 +++ internal/gateway/proxy.go | 51 ++ internal/handler/log.go | 103 ++++ internal/handler/user.go | 585 +++++++++++++++++++++ internal/interfaces/database.go | 16 + internal/interfaces/enum.go | 11 + internal/interfaces/logger.go | 14 + internal/invoke/client.go | 89 ++++ internal/invoke/resolve.go | 82 +++ internal/invoke/resolve_v2.go | 43 ++ internal/invoke/retry.go | 43 ++ internal/log/log.go | 46 ++ internal/middleware/analysis/new.go | 69 +++ internal/middleware/auth/auth.go | 54 ++ internal/middleware/cache/cache.go | 129 +++++ internal/middleware/logger/logger.go | 71 +++ internal/middleware/oplog/new.go | 118 +++++ internal/middleware/oplog/oplog.go | 8 + internal/middleware/privilege/privilege.go | 87 +++ internal/model/es.go | 9 + internal/model/init.go | 89 ++++ internal/model/interface.go | 17 + internal/model/oplog.go | 290 ++++++++++ internal/model/privilege.go | 61 +++ internal/model/role.go | 87 +++ internal/model/token.go | 36 ++ internal/model/user.go | 227 ++++++++ internal/model/writer.go | 34 ++ internal/opt/opt.go | 62 +++ internal/opt/var.go | 55 ++ internal/sqlType/err.go | 9 + internal/sqlType/jsonb.go | 76 +++ internal/sqlType/nullStr.go | 42 ++ internal/sqlType/set.go | 53 ++ internal/sqlType/strSlice.go | 109 ++++ internal/sqlType/uint64Slice.go | 71 +++ internal/tool/ctx.go | 38 ++ internal/tool/human.go | 24 + internal/tool/must.go | 11 + internal/tool/password.go | 84 +++ internal/tool/password_test.go | 11 + internal/tool/random.go | 54 ++ internal/tool/table.go | 124 +++++ internal/tool/tools.go | 19 + internal/unix/handler.go | 59 +++ internal/unix/start.go | 52 ++ main.go | 20 + readme.md | 28 + 76 files changed, 5146 insertions(+) create mode 100644 .gitignore create mode 100644 .nfctl create mode 100644 Dockerfile create mode 100644 deployment/database.dev.yml create mode 100644 deployment/run.sh create mode 100644 etc/config.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 httptest/user.http create mode 100644 internal/api/api.go create mode 100644 internal/api/start.go create mode 100644 internal/cmd/cli.go create mode 100644 internal/cmd/execute.go create mode 100644 internal/cmd/init.go create mode 100644 internal/cmd/root.go create mode 100644 internal/controller/impl.go create mode 100644 internal/controller/user.go create mode 100644 internal/database/cache/cache_lru.go create mode 100644 internal/database/cache/cache_memory.go create mode 100644 internal/database/cache/cache_redis.go create mode 100644 internal/database/cache/client.go create mode 100644 internal/database/cache/error.go create mode 100644 internal/database/cache/init.go create mode 100644 internal/database/db/client.go create mode 100644 internal/database/db/db_test.go create mode 100644 internal/database/db/new.go create mode 100644 internal/database/es/client.go create mode 100644 internal/database/es/raw.go create mode 100644 internal/gateway/gateway.go create mode 100644 internal/gateway/proxy.go create mode 100644 internal/handler/log.go create mode 100644 internal/handler/user.go create mode 100644 internal/interfaces/database.go create mode 100644 internal/interfaces/enum.go create mode 100644 internal/interfaces/logger.go create mode 100644 internal/invoke/client.go create mode 100644 internal/invoke/resolve.go create mode 100644 internal/invoke/resolve_v2.go create mode 100644 internal/invoke/retry.go create mode 100644 internal/log/log.go create mode 100644 internal/middleware/analysis/new.go create mode 100644 internal/middleware/auth/auth.go create mode 100644 internal/middleware/cache/cache.go create mode 100644 internal/middleware/logger/logger.go create mode 100644 internal/middleware/oplog/new.go create mode 100644 internal/middleware/oplog/oplog.go create mode 100644 internal/middleware/privilege/privilege.go create mode 100644 internal/model/es.go create mode 100644 internal/model/init.go create mode 100644 internal/model/interface.go create mode 100644 internal/model/oplog.go create mode 100644 internal/model/privilege.go create mode 100644 internal/model/role.go create mode 100644 internal/model/token.go create mode 100644 internal/model/user.go create mode 100644 internal/model/writer.go create mode 100644 internal/opt/opt.go create mode 100644 internal/opt/var.go create mode 100644 internal/sqlType/err.go create mode 100644 internal/sqlType/jsonb.go create mode 100644 internal/sqlType/nullStr.go create mode 100644 internal/sqlType/set.go create mode 100644 internal/sqlType/strSlice.go create mode 100644 internal/sqlType/uint64Slice.go create mode 100644 internal/tool/ctx.go create mode 100644 internal/tool/human.go create mode 100644 internal/tool/must.go create mode 100644 internal/tool/password.go create mode 100644 internal/tool/password_test.go create mode 100644 internal/tool/random.go create mode 100644 internal/tool/table.go create mode 100644 internal/tool/tools.go create mode 100644 internal/unix/handler.go create mode 100644 internal/unix/start.go create mode 100644 main.go create mode 100644 readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96a9917 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +.vscode +.DS_Store +.data +dist +*.sqlite +*.sqlite3 +xtest +*.sock +__debug* \ No newline at end of file diff --git a/.nfctl b/.nfctl new file mode 100644 index 0000000..cf2cdca --- /dev/null +++ b/.nfctl @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..22697cb --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/deployment/database.dev.yml b/deployment/database.dev.yml new file mode 100644 index 0000000..36a7711 --- /dev/null +++ b/deployment/database.dev.yml @@ -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 \ No newline at end of file diff --git a/deployment/run.sh b/deployment/run.sh new file mode 100644 index 0000000..a0c5cdc --- /dev/null +++ b/deployment/run.sh @@ -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 \ No newline at end of file diff --git a/etc/config.json b/etc/config.json new file mode 100644 index 0000000..7d9c95c --- /dev/null +++ b/etc/config.json @@ -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" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..31f3a2f --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..476c5d3 --- /dev/null +++ b/go.sum @@ -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= diff --git a/httptest/user.http b/httptest/user.http new file mode 100644 index 0000000..2d0a21e --- /dev/null +++ b/httptest/user.http @@ -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" +} diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..bb6af57 --- /dev/null +++ b/internal/api/api.go @@ -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 +} diff --git a/internal/api/start.go b/internal/api/start.go new file mode 100644 index 0000000..2c112ad --- /dev/null +++ b/internal/api/start.go @@ -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 +} diff --git a/internal/cmd/cli.go b/internal/cmd/cli.go new file mode 100644 index 0000000..1bb3c08 --- /dev/null +++ b/internal/cmd/cli.go @@ -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 + }, + }) +} diff --git a/internal/cmd/execute.go b/internal/cmd/execute.go new file mode 100644 index 0000000..a23a4ce --- /dev/null +++ b/internal/cmd/execute.go @@ -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) +} diff --git a/internal/cmd/init.go b/internal/cmd/init.go new file mode 100644 index 0000000..2a619cc --- /dev/null +++ b/internal/cmd/init.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "time" +) + +func init() { + time.Local = time.FixedZone("CST", 8*3600) + + initCli() + + initRoot(cliCommand) +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..8937ef0 --- /dev/null +++ b/internal/cmd/root.go @@ -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...) +} diff --git a/internal/controller/impl.go b/internal/controller/impl.go new file mode 100644 index 0000000..cf41e30 --- /dev/null +++ b/internal/controller/impl.go @@ -0,0 +1,14 @@ +package controller + +import "context" + +var ( + // UserController todo: 可以实现自己的 controller + UserController userController +) + +func Init(ctx context.Context) error { + UserController = uc{} + + return nil +} \ No newline at end of file diff --git a/internal/controller/user.go b/internal/controller/user.go new file mode 100644 index 0000000..cb13c1d --- /dev/null +++ b/internal/controller/user.go @@ -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 +} diff --git a/internal/database/cache/cache_lru.go b/internal/database/cache/cache_lru.go new file mode 100644 index 0000000..7ada4e4 --- /dev/null +++ b/internal/database/cache/cache_lru.go @@ -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 +} diff --git a/internal/database/cache/cache_memory.go b/internal/database/cache/cache_memory.go new file mode 100644 index 0000000..9dc036d --- /dev/null +++ b/internal/database/cache/cache_memory.go @@ -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 +} diff --git a/internal/database/cache/cache_redis.go b/internal/database/cache/cache_redis.go new file mode 100644 index 0000000..05dfc4d --- /dev/null +++ b/internal/database/cache/cache_redis.go @@ -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() +} \ No newline at end of file diff --git a/internal/database/cache/client.go b/internal/database/cache/client.go new file mode 100644 index 0000000..08b9b68 --- /dev/null +++ b/internal/database/cache/client.go @@ -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 +} diff --git a/internal/database/cache/error.go b/internal/database/cache/error.go new file mode 100644 index 0000000..93a36f1 --- /dev/null +++ b/internal/database/cache/error.go @@ -0,0 +1,7 @@ +package cache + +import "errors" + +var ( + ErrorKeyNotFound = errors.New("key not found") +) \ No newline at end of file diff --git a/internal/database/cache/init.go b/internal/database/cache/init.go new file mode 100644 index 0000000..585fa77 --- /dev/null +++ b/internal/database/cache/init.go @@ -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 +} diff --git a/internal/database/db/client.go b/internal/database/db/client.go new file mode 100644 index 0000000..45f1c8a --- /dev/null +++ b/internal/database/db/client.go @@ -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() +} diff --git a/internal/database/db/db_test.go b/internal/database/db/db_test.go new file mode 100644 index 0000000..64a0521 --- /dev/null +++ b/internal/database/db/db_test.go @@ -0,0 +1,8 @@ +package db + +import ( + "testing" +) + +func TestOpen(t *testing.T) { +} diff --git a/internal/database/db/new.go b/internal/database/db/new.go new file mode 100644 index 0000000..409af76 --- /dev/null +++ b/internal/database/db/new.go @@ -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 +} diff --git a/internal/database/es/client.go b/internal/database/es/client.go new file mode 100644 index 0000000..844adad --- /dev/null +++ b/internal/database/es/client.go @@ -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 +} diff --git a/internal/database/es/raw.go b/internal/database/es/raw.go new file mode 100644 index 0000000..910142c --- /dev/null +++ b/internal/database/es/raw.go @@ -0,0 +1 @@ +package es diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go new file mode 100644 index 0000000..04492b5 --- /dev/null +++ b/internal/gateway/gateway.go @@ -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 +} diff --git a/internal/gateway/proxy.go b/internal/gateway/proxy.go new file mode 100644 index 0000000..b69149d --- /dev/null +++ b/internal/gateway/proxy.go @@ -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 +} diff --git a/internal/handler/log.go b/internal/handler/log.go new file mode 100644 index 0000000..4dc0a56 --- /dev/null +++ b/internal/handler/log.go @@ -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}) +} diff --git a/internal/handler/user.go b/internal/handler/user.go new file mode 100644 index 0000000..c829c11 --- /dev/null +++ b/internal/handler/user.go @@ -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, "删除成功") +} diff --git a/internal/interfaces/database.go b/internal/interfaces/database.go new file mode 100644 index 0000000..85d163c --- /dev/null +++ b/internal/interfaces/database.go @@ -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 +} \ No newline at end of file diff --git a/internal/interfaces/enum.go b/internal/interfaces/enum.go new file mode 100644 index 0000000..d26b3c1 --- /dev/null +++ b/internal/interfaces/enum.go @@ -0,0 +1,11 @@ +package interfaces + +type Enum interface { + Value() int64 + Code() string + Label() string + + MarshalJSON() ([]byte, error) + + All() []Enum +} \ No newline at end of file diff --git a/internal/interfaces/logger.go b/internal/interfaces/logger.go new file mode 100644 index 0000000..6105a51 --- /dev/null +++ b/internal/interfaces/logger.go @@ -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) +} \ No newline at end of file diff --git a/internal/invoke/client.go b/internal/invoke/client.go new file mode 100644 index 0000000..26aeb05 --- /dev/null +++ b/internal/invoke/client.go @@ -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 +} diff --git a/internal/invoke/resolve.go b/internal/invoke/resolve.go new file mode 100644 index 0000000..be14c44 --- /dev/null +++ b/internal/invoke/resolve.go @@ -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 +} \ No newline at end of file diff --git a/internal/invoke/resolve_v2.go b/internal/invoke/resolve_v2.go new file mode 100644 index 0000000..05498e7 --- /dev/null +++ b/internal/invoke/resolve_v2.go @@ -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{} +) \ No newline at end of file diff --git a/internal/invoke/retry.go b/internal/invoke/retry.go new file mode 100644 index 0000000..41eb993 --- /dev/null +++ b/internal/invoke/retry.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..5e7ba0d --- /dev/null +++ b/internal/log/log.go @@ -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...) +} \ No newline at end of file diff --git a/internal/middleware/analysis/new.go b/internal/middleware/analysis/new.go new file mode 100644 index 0000000..8b552ae --- /dev/null +++ b/internal/middleware/analysis/new.go @@ -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() + } +} diff --git a/internal/middleware/auth/auth.go b/internal/middleware/auth/auth.go new file mode 100644 index 0000000..7008a7c --- /dev/null +++ b/internal/middleware/auth/auth.go @@ -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() + } +} diff --git a/internal/middleware/cache/cache.go b/internal/middleware/cache/cache.go new file mode 100644 index 0000000..a88b884 --- /dev/null +++ b/internal/middleware/cache/cache.go @@ -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 + } +} diff --git a/internal/middleware/logger/logger.go b/internal/middleware/logger/logger.go new file mode 100644 index 0000000..acf457a --- /dev/null +++ b/internal/middleware/logger/logger.go @@ -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 + } +} diff --git a/internal/middleware/oplog/new.go b/internal/middleware/oplog/new.go new file mode 100644 index 0000000..0372abf --- /dev/null +++ b/internal/middleware/oplog/new.go @@ -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 + } +} diff --git a/internal/middleware/oplog/oplog.go b/internal/middleware/oplog/oplog.go new file mode 100644 index 0000000..d086d8f --- /dev/null +++ b/internal/middleware/oplog/oplog.go @@ -0,0 +1,8 @@ +package oplog + +import "esway/internal/model" + +type OpLog struct { + Type model.OpLogType + Content map[string]any +} diff --git a/internal/middleware/privilege/privilege.go b/internal/middleware/privilege/privilege.go new file mode 100644 index 0000000..a7ed6e5 --- /dev/null +++ b/internal/middleware/privilege/privilege.go @@ -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() + } +} diff --git a/internal/model/es.go b/internal/model/es.go new file mode 100644 index 0000000..0f6f8f8 --- /dev/null +++ b/internal/model/es.go @@ -0,0 +1,9 @@ +package model + +type ESReqRes struct { + ClientRequest bool // 是否是 es client 的请求 + Path string + Method string + Indixes []string + Api ESApi +} diff --git a/internal/model/init.go b/internal/model/init.go new file mode 100644 index 0000000..c76bc67 --- /dev/null +++ b/internal/model/init.go @@ -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 +} diff --git a/internal/model/interface.go b/internal/model/interface.go new file mode 100644 index 0000000..ee0eab5 --- /dev/null +++ b/internal/model/interface.go @@ -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 +} \ No newline at end of file diff --git a/internal/model/oplog.go b/internal/model/oplog.go new file mode 100644 index 0000000..eb977f1 --- /dev/null +++ b/internal/model/oplog.go @@ -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 = ` +
+ 用户 + {{ .username }} + + 于 + {{ time_format .time "2006-01-02 15:04:05" }} + + 在 + {{ .ip }} + + 上 + + 登入 + + 了系统 +
+ ` + oplogTemplateLogout = ` +
+ 用户 + {{ .username }} + + 于 + {{ time_format .time "2006-01-02 15:04:05" }} + + 在 + {{ .ip }} + + 上 + + 登出 + + 了系统 +
+` + oplogTemplateCreateUser = ` +
+ 用户 + {{ .username }} + + 于 + {{ time_format .time "2006-01-02 15:04:05" }} + + + 创建 + + 了用户 + {{ .target_username }} + +
+` + oplogTemplateUpdateUser = ` +
+ 用户 + {{ .username }} + + 于 + {{ time_format .time "2006-01-02 15:04:05" }} + + + 编辑 + + 了用户 + {{ .target_username }} + +
+` + oplogTemplateDeleteUser = ` +
+ 用户 + {{ .username }} + + 于 + {{ time_format .time "2006-01-02 15:04:05" }} + + + 删除 + + 了用户 + {{ .target_username }} + +
+` +) + +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 `
错误的日志类型
` + } +} + +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:"-"` +} diff --git a/internal/model/privilege.go b/internal/model/privilege.go new file mode 100644 index 0000000..ec3ad0d --- /dev/null +++ b/internal/model/privilege.go @@ -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) \ No newline at end of file diff --git a/internal/model/role.go b/internal/model/role.go new file mode 100644 index 0000000..5105bd8 --- /dev/null +++ b/internal/model/role.go @@ -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 +} diff --git a/internal/model/token.go b/internal/model/token.go new file mode 100644 index 0000000..dd4fcfd --- /dev/null +++ b/internal/model/token.go @@ -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" +) diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..6577e2b --- /dev/null +++ b/internal/model/user.go @@ -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, + }) +} diff --git a/internal/model/writer.go b/internal/model/writer.go new file mode 100644 index 0000000..e40804c --- /dev/null +++ b/internal/model/writer.go @@ -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} +} diff --git a/internal/opt/opt.go b/internal/opt/opt.go new file mode 100644 index 0000000..c1abd0c --- /dev/null +++ b/internal/opt/opt.go @@ -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 +} diff --git a/internal/opt/var.go b/internal/opt/var.go new file mode 100644 index 0000000..de45b13 --- /dev/null +++ b/internal/opt/var.go @@ -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 +) diff --git a/internal/sqlType/err.go b/internal/sqlType/err.go new file mode 100644 index 0000000..b407ebd --- /dev/null +++ b/internal/sqlType/err.go @@ -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") +) \ No newline at end of file diff --git a/internal/sqlType/jsonb.go b/internal/sqlType/jsonb.go new file mode 100644 index 0000000..296bc0d --- /dev/null +++ b/internal/sqlType/jsonb.go @@ -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) +} \ No newline at end of file diff --git a/internal/sqlType/nullStr.go b/internal/sqlType/nullStr.go new file mode 100644 index 0000000..360d866 --- /dev/null +++ b/internal/sqlType/nullStr.go @@ -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 +} \ No newline at end of file diff --git a/internal/sqlType/set.go b/internal/sqlType/set.go new file mode 100644 index 0000000..363e76b --- /dev/null +++ b/internal/sqlType/set.go @@ -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{}{} + } +} \ No newline at end of file diff --git a/internal/sqlType/strSlice.go b/internal/sqlType/strSlice.go new file mode 100644 index 0000000..85d6d7f --- /dev/null +++ b/internal/sqlType/strSlice.go @@ -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 +} \ No newline at end of file diff --git a/internal/sqlType/uint64Slice.go b/internal/sqlType/uint64Slice.go new file mode 100644 index 0000000..41ca9e4 --- /dev/null +++ b/internal/sqlType/uint64Slice.go @@ -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 +} \ No newline at end of file diff --git a/internal/tool/ctx.go b/internal/tool/ctx.go new file mode 100644 index 0000000..1abe889 --- /dev/null +++ b/internal/tool/ctx.go @@ -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 +} \ No newline at end of file diff --git a/internal/tool/human.go b/internal/tool/human.go new file mode 100644 index 0000000..c8ec7f2 --- /dev/null +++ b/internal/tool/human.go @@ -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) +} \ No newline at end of file diff --git a/internal/tool/must.go b/internal/tool/must.go new file mode 100644 index 0000000..16d49a6 --- /dev/null +++ b/internal/tool/must.go @@ -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()) + } + } +} \ No newline at end of file diff --git a/internal/tool/password.go b/internal/tool/password.go new file mode 100644 index 0000000..81b6460 --- /dev/null +++ b/internal/tool/password.go @@ -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 +} \ No newline at end of file diff --git a/internal/tool/password_test.go b/internal/tool/password_test.go new file mode 100644 index 0000000..d7d8985 --- /dev/null +++ b/internal/tool/password_test.go @@ -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) +} \ No newline at end of file diff --git a/internal/tool/random.go b/internal/tool/random.go new file mode 100644 index 0000000..0b0c4ab --- /dev/null +++ b/internal/tool/random.go @@ -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) +} \ No newline at end of file diff --git a/internal/tool/table.go b/internal/tool/table.go new file mode 100644 index 0000000..a1847c6 --- /dev/null +++ b/internal/tool/table.go @@ -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}) + } +} \ No newline at end of file diff --git a/internal/tool/tools.go b/internal/tool/tools.go new file mode 100644 index 0000000..c0f3102 --- /dev/null +++ b/internal/tool/tools.go @@ -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 +} \ No newline at end of file diff --git a/internal/unix/handler.go b/internal/unix/handler.go new file mode 100644 index 0000000..bbe4d0c --- /dev/null +++ b/internal/unix/handler.go @@ -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 +} diff --git a/internal/unix/start.go b/internal/unix/start.go new file mode 100644 index 0000000..693dece --- /dev/null +++ b/internal/unix/start.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fcda1bd --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ceae3c4 --- /dev/null +++ b/readme.md @@ -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://: + I18N_LOCALE: zh-CN + network_mode: "host" + +``` \ No newline at end of file