Compare commits

59 Commits

Author SHA1 Message Date
49e02d3d79 Fix gitea workflows
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Has been cancelled
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Backend Build (NixCN CMS) TeamCity build failed
Client CMS Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-27 17:54:23 +00:00
1927dd6a8c Merge pull request 'Fix gitea workflow name' (#8) from develop into main
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Has been cancelled
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Reviewed-on: nixcn/nixcn-cms#8
2026-01-27 17:48:58 +00:00
d90e22b641 Fix gitea workflow name
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Failing after 1m15s
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:48:18 +08:00
ad521e04ae Merge pull request 'First merge from develop to main (WIP)' (#7) from develop into main
Reviewed-on: nixcn/nixcn-cms#7
2026-01-27 17:47:05 +00:00
4f7632af53 Fix gitea workflow
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:41:39 +08:00
ca080f4e2a Add gitea action workflow
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:36:54 +08:00
5a5239e335 Optomize user list service query bind struct
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-24 12:48:11 +08:00
314995e5f9 Finilize user api layer
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-24 12:37:17 +08:00
8e11ba4631 WIP: Full restruct, seprate service and api
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-24 11:42:35 +08:00
dfd5532b20 Change default config
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:39:20 +08:00
986f63c0af Add context for all exceptions
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:37:20 +08:00
154c929859 Change postgres db instance name
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:25:58 +08:00
f779435cf0 Devenv backend wait for 30s to boot
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:22:07 +08:00
5f6eb9f2a2 Trace back everything (tested)
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:19:17 +08:00
3f44d2d9c2 Add otel tracer
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 16:59:53 +08:00
b8f89ab655 Add context for everything
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 16:43:46 +08:00
83df018d34 Only enable file log in debug mode
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:52:44 +08:00
7b3fe24b7c Add ErrorHandler for log level selects
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:48:49 +08:00
75c4edfa3d fix(client): remove console.log
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 15:38:31 +08:00
a060901cc3 refactor(client): improve token handler stability
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 15:37:04 +08:00
8e41514d05 Fix stupid ai bug
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:30:50 +08:00
9aff7d8f26 Fix 200 response exception builder
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:26:59 +08:00
2f26b2ddb5 Fix stupid ai errors
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 14:58:23 +08:00
96d76b3657 feat(client): bio editor
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 14:51:03 +08:00
4e45a9b6d0 feat(client): update userinfo
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 14:49:49 +08:00
27ac4d9b4a feat: sync api changes and fix auth-related bugs
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 14:49:49 +08:00
a60a796345 refactor: use SetError in exception.Builder where errors are available
Update multiple services and middlewares to pass the original error to exception.Builder before building the error code.

Co-authored-by: Gemini <gemini@google.com>
2026-01-21 14:42:52 +08:00
14f50ecdb2 refactor: update exception constants to follow new naming convention
- Update old ErrorStatus, ErrorType, and Service/Endpoint constants to new naming convention
- Fix incorrect TypeSpecific usage in JWT middleware
- Add missing event specific error definitions to specific.yaml
- Regenerate exception constants

Co-authored-by: Gemini <gemini@google.com>
2026-01-21 14:34:09 +08:00
b1c78dce28 Add Build error hook (print exception)
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 14:28:23 +08:00
585ec46282 Fix some type change bugs (error)
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 14:16:47 +08:00
8f69b61799 Mod justfile
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:59:03 +08:00
64bab332c9 Mod justfile
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:58:42 +08:00
38401a5f69 Mod justfile
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:57:06 +08:00
f03d472c30 Ignore generated go files
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:55:05 +08:00
2d6f6700f0 Move definitions to gen_exception
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:53:28 +08:00
2e11fc5d9c Fix go mod
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:51:37 +08:00
ac428946e7 Use generator to generate exceptions from yaml
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 13:48:37 +08:00
e4329dfc2b refactor: standardize error handling with exception.Builder
- Replace hardcoded error messages with structured error codes using exception.Builder.
- Introduce new common error constants in exception/common.go (CommonErrorInvalidInput, CommonErrorUserNotFound, etc.).
- Update exception/specific.go with domain-specific errors and remove redundant ones.
- Apply consistent error handling across auth, event, user services and middleware.

Co-authored-by: Gemini <gemini@google.com>
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 12:47:49 +08:00
5dbbdc62e6 Add exception error manager
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 12:04:17 +08:00
200614a5c9 Add error retern for database
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 10:03:56 +08:00
4ac5b1c101 Fix error reponses
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 10:01:13 +08:00
b7e6009706 Change logrus to slog
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:52:54 +08:00
fd262239e4 Remove file logger from config
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:32:13 +08:00
cf761d218d Fix gin debug mode
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:31:24 +08:00
110627f27e Fix gin logger
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:29:01 +08:00
64392c32c6 Restruct logger order
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:25:10 +08:00
3f8f2547be Split and optimize gin logger
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:13:13 +08:00
632fa6cf8e Fix config types
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:03:12 +08:00
d04f8cdc44 Move email send from to send func
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 09:02:29 +08:00
97f5677a97 Remove oauth login email
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 08:58:14 +08:00
2ed4a4da02 User update check one by one
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 23:33:50 +08:00
100fe32f8e Disable email changes, lazy~~~~~
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 20:02:54 +08:00
231f591767 Fix bind json error
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 19:48:59 +08:00
0e7aaed154 Fix typo
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 19:10:00 +08:00
89c2d11f19 Fix exchange bind json error
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 19:04:26 +08:00
cd93491d98 Add exchange api endpoint, fix jwt authtoken var type error
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 18:51:15 +08:00
9b83ab565a Fix response structure error and router error
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 17:48:52 +08:00
5e17bbd965 Fix Containerfile using just build
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 16:20:44 +08:00
de0d05df0a Add charts empty folder
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-20 16:14:26 +08:00
95 changed files with 3415 additions and 1159 deletions

View File

@@ -1 +1,2 @@
TZ=Asia/Shanghai TZ=Asia/Shanghai
LOG_LEVEL=debug

View File

@@ -0,0 +1,53 @@
name: Check build frontend and backend
run-name: ${{ gitea.actor }} is building nixcn-cms check
on: [push]
jobs:
build-frontend:
name: Build PNPM Frontend
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Corepack
run: npm install corepack
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: pnpm install
- name: Build frontend
run: pnpm build
build-backend:
name: Build Go Backend
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25.5"
cache: false
- name: Install dependencies
run: go mod tidy
- name: Generate go dependencies
run: go generate .
- name: Build backend
run: go build -v -o server main.go
- name: Run Tests
run: go test ./...

3
.gitignore vendored
View File

@@ -46,3 +46,6 @@ go.work.sum
.DS_Store .DS_Store
__MACOSX __MACOSX
._* ._*
# go gen
*_gen.go

View File

@@ -1,16 +1,15 @@
FROM docker.io/node:22-alpine AS client-build FROM docker.io/node:22-alpine AS client-cms-build
RUN apk add just -y
RUN npm install -g corepack && \ RUN npm install -g corepack && \
corepack enable corepack enable
WORKDIR /app WORKDIR /app
ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL
COPY . . COPY . .
RUN cd /app/client && \ RUN just build-client-cms
pnpm install -r --frozen-lockfile && \
pnpm run build
FROM docker.io/busybox:1.37 AS client FROM docker.io/busybox:1.37 AS client-cms
WORKDIR /app WORKDIR /app
COPY --from=client-build /app/client/dist . COPY --from=client-build /app/.outputs/client/cms/dist .
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["httpd", "-f", "-p", "3000", "-h", "/app", "-v"] ENTRYPOINT ["httpd", "-f", "-p", "3000", "-h", "/app", "-v"]

8
api/auth/handler.go Normal file
View File

@@ -0,0 +1,8 @@
package auth
import (
"github.com/gin-gonic/gin"
)
func ApiHandler(r *gin.RouterGroup) {
}

11
api/event/handler.go Normal file
View File

@@ -0,0 +1,11 @@
package event
import (
"nixcn-cms/middleware"
"github.com/gin-gonic/gin"
)
func ApiHandler(r *gin.RouterGroup) {
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
}

17
api/handler.go Normal file
View File

@@ -0,0 +1,17 @@
package api
import (
"nixcn-cms/api/auth"
"nixcn-cms/api/event"
"nixcn-cms/api/kyc"
"nixcn-cms/api/user"
"github.com/gin-gonic/gin"
)
func Handler(r *gin.RouterGroup) {
auth.ApiHandler(r.Group("/auth"))
user.ApiHandler(r.Group("/user"))
event.ApiHandler(r.Group("/event"))
kyc.ApiHandler(r.Group("/kyc"))
}

11
api/kyc/handler.go Normal file
View File

@@ -0,0 +1,11 @@
package kyc
import (
"nixcn-cms/middleware"
"github.com/gin-gonic/gin"
)
func ApiHandler(r *gin.RouterGroup) {
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
}

View File

@@ -2,6 +2,5 @@ package user
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
func Create(c *gin.Context) { func (self *UserHandler) Create(c *gin.Context) {
} }

24
api/user/full.go Normal file
View File

@@ -0,0 +1,24 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
func (self *UserHandler) Full(c *gin.Context) {
userTablePayload := &service.UserTablePayload{
Context: c,
}
result := self.svc.GetUserFullTable(userTablePayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

24
api/user/handler.go Normal file
View File

@@ -0,0 +1,24 @@
package user
import (
"nixcn-cms/middleware"
"nixcn-cms/service"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
svc service.UserService
}
func ApiHandler(r *gin.RouterGroup) {
userSvc := service.NewUserService()
userHandler := &UserHandler{userSvc}
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(5))
r.GET("/info", userHandler.Info)
r.PATCH("/update", userHandler.Update)
r.GET("/list", middleware.Permission(20), userHandler.List)
r.POST("/full", middleware.Permission(40), userHandler.Full)
r.POST("/create", middleware.Permission(50), userHandler.Create)
}

56
api/user/info.go Normal file
View File

@@ -0,0 +1,56 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func (self *UserHandler) Info(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Throw(c).
String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 500, errorCode)
return
}
UserInfoPayload := &service.UserInfoPayload{
Context: c,
UserId: userId,
Data: nil,
}
result := self.svc.GetUserInfo(UserInfoPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

46
api/user/list.go Normal file
View File

@@ -0,0 +1,46 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
func (self *UserHandler) List(c *gin.Context) {
type ListQuery struct {
Limit *string `form:"limit"`
Offset *string `form:"offset"`
}
var query ListQuery
if err := c.ShouldBindQuery(&query); err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Throw(c).
String()
utils.HttpResponse(c, 400, exception)
return
}
userListPayload := &service.UserListPayload{
Context: c,
Limit: query.Limit,
Offset: query.Offset,
}
result := self.svc.ListUsers(userListPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

69
api/user/update.go Normal file
View File

@@ -0,0 +1,69 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func (self *UserHandler) Update(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Throw(c).
String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
Throw(c).
String()
utils.HttpResponse(c, 500, errorCode)
return
}
userInfoPayload := &service.UserInfoPayload{
Context: c,
UserId: userId,
}
err = c.ShouldBindJSON(&userInfoPayload.Data)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.UpdateUserInfo(userInfoPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

0
charts/.gitkeep Normal file
View File

View File

@@ -3,7 +3,7 @@ import pluginQuery from '@tanstack/eslint-plugin-query';
export default antfu({ export default antfu({
gitignore: true, gitignore: true,
ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**'], ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*'],
react: true, react: true,
stylistic: { stylistic: {
semi: true, semi: true,

View File

@@ -37,6 +37,7 @@
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tanstack/zod-adapter": "^1.143.4", "@tanstack/zod-adapter": "^1.143.4",
"@tanstack/zod-form-adapter": "^0.42.1", "@tanstack/zod-form-adapter": "^0.42.1",
"@uiw/react-md-editor": "^4.0.11",
"axios": "^1.13.2", "axios": "^1.13.2",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
@@ -44,6 +45,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"culori": "^4.0.2", "culori": "^4.0.2",
"immer": "^11.1.0", "immer": "^11.1.0",
"lodash-es": "^4.17.22",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@@ -69,6 +71,7 @@
"@tanstack/router-plugin": "^1.141.7", "@tanstack/router-plugin": "^1.141.7",
"@types/base-64": "^1.0.2", "@types/base-64": "^1.0.2",
"@types/culori": "^4.0.1", "@types/culori": "^4.0.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/react": "^19.2.5", "@types/react": "^19.2.5",
@@ -82,6 +85,7 @@
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.13.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"type-fest": "^5.4.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.46.4",
"vite": "^7.2.4", "vite": "^7.2.4",

View File

@@ -89,6 +89,9 @@ importers:
'@tanstack/zod-form-adapter': '@tanstack/zod-form-adapter':
specifier: ^0.42.1 specifier: ^0.42.1
version: 0.42.1(zod@4.3.5) version: 0.42.1(zod@4.3.5)
'@uiw/react-md-editor':
specifier: ^4.0.11
version: 4.0.11(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
axios: axios:
specifier: ^1.13.2 specifier: ^1.13.2
version: 1.13.2 version: 1.13.2
@@ -110,6 +113,9 @@ importers:
immer: immer:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.3 version: 11.1.3
lodash-es:
specifier: ^4.17.22
version: 4.17.22
lucide-react: lucide-react:
specifier: ^0.562.0 specifier: ^0.562.0
version: 0.562.0(react@19.2.3) version: 0.562.0(react@19.2.3)
@@ -180,6 +186,9 @@ importers:
'@types/culori': '@types/culori':
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@types/node': '@types/node':
specifier: ^25.0.3 specifier: ^25.0.3
version: 25.0.9 version: 25.0.9
@@ -219,6 +228,9 @@ importers:
tw-animate-css: tw-animate-css:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
type-fest:
specifier: ^5.4.1
version: 5.4.1
typescript: typescript:
specifier: ~5.9.3 specifier: ~5.9.3
version: 5.9.3 version: 5.9.3
@@ -1254,66 +1266,79 @@ packages:
resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.55.2': '@rollup/rollup-linux-arm-musleabihf@4.55.2':
resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.55.2': '@rollup/rollup-linux-arm64-gnu@4.55.2':
resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.55.2': '@rollup/rollup-linux-arm64-musl@4.55.2':
resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.55.2': '@rollup/rollup-linux-loong64-gnu@4.55.2':
resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.55.2': '@rollup/rollup-linux-loong64-musl@4.55.2':
resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.55.2': '@rollup/rollup-linux-ppc64-gnu@4.55.2':
resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.55.2': '@rollup/rollup-linux-ppc64-musl@4.55.2':
resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.55.2': '@rollup/rollup-linux-riscv64-gnu@4.55.2':
resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.55.2': '@rollup/rollup-linux-riscv64-musl@4.55.2':
resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.55.2': '@rollup/rollup-linux-s390x-gnu@4.55.2':
resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.55.2': '@rollup/rollup-linux-x64-gnu@4.55.2':
resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.55.2': '@rollup/rollup-linux-x64-musl@4.55.2':
resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.55.2': '@rollup/rollup-openbsd-x64@4.55.2':
resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==}
@@ -1472,24 +1497,28 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18': '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18': '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18': '@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18': '@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -1724,12 +1753,21 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/hast@2.3.10':
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
'@types/hast@3.0.4': '@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.23':
resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -1739,6 +1777,9 @@ packages:
'@types/node@25.0.9': '@types/node@25.0.9':
resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/qrcode@1.5.6': '@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
@@ -1818,6 +1859,21 @@ packages:
resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@uiw/copy-to-clipboard@1.0.19':
resolution: {integrity: sha512-AYxzFUBkZrhtExb2QC0C4lFH2+BSx6JVId9iqeGHakBuosqiQHUQaNZCvIBeM97Ucp+nJ22flOh8FBT2pKRRAA==}
'@uiw/react-markdown-preview@5.1.5':
resolution: {integrity: sha512-DNOqx1a6gJR7Btt57zpGEKTfHRlb7rWbtctMRO2f82wWcuoJsxPBrM+JWebDdOD0LfD8oe2CQvW2ICQJKHQhZg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@uiw/react-md-editor@4.0.11':
resolution: {integrity: sha512-F0OR5O1v54EkZYvJj3ew0I7UqLiPeU34hMAY4MdXS3hI86rruYi5DHVkG/VuvLkUZW7wIETM2QFtZ459gKIjQA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -1936,6 +1992,9 @@ packages:
resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==}
hasBin: true hasBin: true
bcp-47-match@2.0.3:
resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2106,6 +2165,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
css-selector-parser@3.3.0:
resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==}
cssesc@3.0.0: cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2213,6 +2275,10 @@ packages:
dijkstrajs@1.0.3: dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
direction@2.0.1:
resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
hasBin: true
dom-helpers@5.2.1: dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@@ -2244,6 +2310,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
entities@7.0.0: entities@7.0.0:
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@@ -2724,12 +2794,54 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hast-util-from-html@2.0.3:
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-has-property@3.0.0:
resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
hast-util-heading-rank@3.0.0:
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
hast-util-is-element@3.0.0:
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
hast-util-parse-selector@3.1.1:
resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==}
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
hast-util-raw@9.1.0:
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
hast-util-select@6.0.4:
resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==}
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
hast-util-to-jsx-runtime@2.3.6: hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
hast-util-to-parse5@8.0.1:
resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
hast-util-to-string@3.0.1:
resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
hast-util-whitespace@3.0.0: hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
hastscript@7.2.0:
resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==}
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
hermes-estree@0.25.1: hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
@@ -2742,6 +2854,9 @@ packages:
html-url-attributes@3.0.1: html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
ieee754@1.2.1: ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -2922,24 +3037,28 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2: lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2: lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2: lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2: lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -2981,6 +3100,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
lodash-es@4.17.22:
resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==}
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3274,9 +3396,15 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'} engines: {node: '>=8'}
parse-numeric-range@1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
parse-statements@1.0.11: parse-statements@1.0.11:
resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
path-exists@4.0.0: path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -3349,6 +3477,9 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
@@ -3390,6 +3521,12 @@ packages:
'@types/react': '>=18' '@types/react': '>=18'
react: '>=18' react: '>=18'
react-markdown@9.0.3:
resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
peerDependencies:
'@types/react': '>=18'
react: '>=18'
react-refresh@0.18.0: react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3462,6 +3599,9 @@ packages:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
refractor@4.9.0:
resolution: {integrity: sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==}
regexp-ast-analysis@0.7.1: regexp-ast-analysis@0.7.1:
resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -3474,12 +3614,58 @@ packages:
resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
hasBin: true hasBin: true
rehype-attr@3.0.3:
resolution: {integrity: sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==}
engines: {node: '>=16'}
rehype-autolink-headings@7.1.0:
resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
rehype-ignore@2.0.3:
resolution: {integrity: sha512-IzhP6/u/6sm49sdktuYSmeIuObWB+5yC/5eqVws8BhuGA9kY25/byz6uCy/Ravj6lXUShEd2ofHM5MyAIj86Sg==}
engines: {node: '>=16'}
rehype-parse@9.0.1:
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
rehype-prism-plus@2.0.0:
resolution: {integrity: sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==}
rehype-prism-plus@2.0.1:
resolution: {integrity: sha512-Wglct0OW12tksTUseAPyWPo3srjBOY7xKlql/DPKi7HbsdZTyaLCAoO58QBKSczFQxElTsQlOY3JDOFzB/K++Q==}
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
rehype-rewrite@4.0.4:
resolution: {integrity: sha512-L/FO96EOzSA6bzOam4DVu61/PB3AGKcSPXpa53yMIozoxH4qg1+bVZDF8zh1EsuxtSauAhzt5cCnvoplAaSLrw==}
engines: {node: '>=16.0.0'}
rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
rehype-stringify@10.0.1:
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
rehype@13.0.2:
resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
remark-github-blockquote-alert@1.3.1:
resolution: {integrity: sha512-OPNnimcKeozWN1w8KVQEuHOxgN3L4rah8geMOLhA5vN9wITqU4FWD+G26tkEsCGHiOVDbISx+Se5rGZ+D1p0Jg==}
engines: {node: '>=16'}
remark-parse@11.0.0: remark-parse@11.0.0:
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
remark-rehype@11.1.2: remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
require-directory@2.1.1: require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3650,6 +3836,10 @@ packages:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tailwind-merge@3.4.0: tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
@@ -3721,6 +3911,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
type-fest@5.4.1:
resolution: {integrity: sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==}
engines: {node: '>=20'}
typescript-eslint@8.53.1: typescript-eslint@8.53.1:
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3742,6 +3936,9 @@ packages:
unified@11.0.5: unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
unist-util-filter@5.0.1:
resolution: {integrity: sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==}
unist-util-is@6.0.1: unist-util-is@6.0.1:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
@@ -3807,6 +4004,9 @@ packages:
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
vfile-message@4.0.3: vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -3867,6 +4067,9 @@ packages:
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
webpack-virtual-modules@0.6.2: webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
@@ -5386,12 +5589,22 @@ snapshots:
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/hast@2.3.10':
dependencies:
'@types/unist': 2.0.11
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.23
'@types/lodash@4.17.23': {}
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@@ -5402,6 +5615,8 @@ snapshots:
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
'@types/prismjs@1.26.5': {}
'@types/qrcode@1.5.6': '@types/qrcode@1.5.6':
dependencies: dependencies:
'@types/node': 25.0.9 '@types/node': 25.0.9
@@ -5511,6 +5726,41 @@ snapshots:
'@typescript-eslint/types': 8.53.1 '@typescript-eslint/types': 8.53.1
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
'@uiw/copy-to-clipboard@1.0.19': {}
'@uiw/react-markdown-preview@5.1.5(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@babel/runtime': 7.28.6
'@uiw/copy-to-clipboard': 1.0.19
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
react-markdown: 9.0.3(@types/react@19.2.8)(react@19.2.3)
rehype-attr: 3.0.3
rehype-autolink-headings: 7.1.0
rehype-ignore: 2.0.3
rehype-prism-plus: 2.0.0
rehype-raw: 7.0.0
rehype-rewrite: 4.0.4
rehype-slug: 6.0.0
remark-gfm: 4.0.1
remark-github-blockquote-alert: 1.3.1
unist-util-visit: 5.0.0
transitivePeerDependencies:
- '@types/react'
- supports-color
'@uiw/react-md-editor@4.0.11(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@babel/runtime': 7.28.6
'@uiw/react-markdown-preview': 5.1.5(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
rehype: 13.0.2
rehype-prism-plus: 2.0.1
transitivePeerDependencies:
- '@types/react'
- supports-color
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
@@ -5642,6 +5892,8 @@ snapshots:
baseline-browser-mapping@2.9.15: {} baseline-browser-mapping@2.9.15: {}
bcp-47-match@2.0.3: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
birecord@0.1.1: {} birecord@0.1.1: {}
@@ -5798,6 +6050,8 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
css-selector-parser@3.3.0: {}
cssesc@3.0.0: {} cssesc@3.0.0: {}
csstype@3.2.3: {} csstype@3.2.3: {}
@@ -5874,6 +6128,8 @@ snapshots:
dijkstrajs@1.0.3: {} dijkstrajs@1.0.3: {}
direction@2.0.1: {}
dom-helpers@5.2.1: dom-helpers@5.2.1:
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
@@ -5905,6 +6161,8 @@ snapshots:
entities@4.5.0: {} entities@4.5.0: {}
entities@6.0.1: {}
entities@7.0.0: {} entities@7.0.0: {}
environment@1.1.0: {} environment@1.1.0: {}
@@ -6503,6 +6761,94 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hast-util-from-html@2.0.3:
dependencies:
'@types/hast': 3.0.4
devlop: 1.1.0
hast-util-from-parse5: 8.0.3
parse5: 7.3.0
vfile: 6.0.3
vfile-message: 4.0.3
hast-util-from-parse5@8.0.3:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
devlop: 1.1.0
hastscript: 9.0.1
property-information: 7.1.0
vfile: 6.0.3
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-has-property@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-heading-rank@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-is-element@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-parse-selector@3.1.1:
dependencies:
'@types/hast': 2.3.10
hast-util-parse-selector@4.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw@9.1.0:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
'@ungap/structured-clone': 1.3.0
hast-util-from-parse5: 8.0.3
hast-util-to-parse5: 8.0.1
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.1
parse5: 7.3.0
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.3
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-select@6.0.4:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
bcp-47-match: 2.0.3
comma-separated-tokens: 2.0.3
css-selector-parser: 3.3.0
devlop: 1.1.0
direction: 2.0.1
hast-util-has-property: 3.0.0
hast-util-to-string: 3.0.1
hast-util-whitespace: 3.0.0
nth-check: 2.1.1
property-information: 7.1.0
space-separated-tokens: 2.0.2
unist-util-visit: 5.0.0
zwitch: 2.0.4
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
comma-separated-tokens: 2.0.3
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.1
property-information: 7.1.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-to-jsx-runtime@2.3.6: hast-util-to-jsx-runtime@2.3.6:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -6523,10 +6869,40 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
hast-util-to-parse5@8.0.1:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
devlop: 1.1.0
property-information: 7.1.0
space-separated-tokens: 2.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-to-string@3.0.1:
dependencies:
'@types/hast': 3.0.4
hast-util-whitespace@3.0.0: hast-util-whitespace@3.0.0:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
hastscript@7.2.0:
dependencies:
'@types/hast': 2.3.10
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 3.1.1
property-information: 6.5.0
space-separated-tokens: 2.0.2
hastscript@9.0.1:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 7.1.0
space-separated-tokens: 2.0.2
hermes-estree@0.25.1: {} hermes-estree@0.25.1: {}
hermes-parser@0.25.1: hermes-parser@0.25.1:
@@ -6537,6 +6913,8 @@ snapshots:
html-url-attributes@3.0.1: {} html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
ieee754@1.2.1: {} ieee754@1.2.1: {}
ignore@5.3.2: {} ignore@5.3.2: {}
@@ -6733,6 +7111,8 @@ snapshots:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
lodash-es@4.17.22: {}
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
lodash@4.17.21: {} lodash@4.17.21: {}
@@ -7253,8 +7633,14 @@ snapshots:
json-parse-even-better-errors: 2.3.1 json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
parse-numeric-range@1.3.0: {}
parse-statements@1.0.11: {} parse-statements@1.0.11: {}
parse5@7.3.0:
dependencies:
entities: 6.0.1
path-exists@4.0.0: {} path-exists@4.0.0: {}
path-key@3.1.1: {} path-key@3.1.1: {}
@@ -7317,6 +7703,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
property-information@6.5.0: {}
property-information@7.1.0: {} property-information@7.1.0: {}
proxy-from-env@1.1.0: {} proxy-from-env@1.1.0: {}
@@ -7362,6 +7750,23 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
react-markdown@9.0.3(@types/react@19.2.8)(react@19.2.3):
dependencies:
'@types/hast': 3.0.4
'@types/react': 19.2.8
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.6
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.1
react: 19.2.3
remark-parse: 11.0.0
remark-rehype: 11.1.2
unified: 11.0.5
unist-util-visit: 5.0.0
vfile: 6.0.3
transitivePeerDependencies:
- supports-color
react-refresh@0.18.0: {} react-refresh@0.18.0: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3):
@@ -7443,6 +7848,13 @@ snapshots:
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
refractor@4.9.0:
dependencies:
'@types/hast': 2.3.10
'@types/prismjs': 1.26.5
hastscript: 7.2.0
parse-entities: 4.0.2
regexp-ast-analysis@0.7.1: regexp-ast-analysis@0.7.1:
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
@@ -7454,6 +7866,98 @@ snapshots:
dependencies: dependencies:
jsesc: 3.1.0 jsesc: 3.1.0
rehype-attr@3.0.3:
dependencies:
unified: 11.0.5
unist-util-visit: 5.0.0
rehype-autolink-headings@7.1.0:
dependencies:
'@types/hast': 3.0.4
'@ungap/structured-clone': 1.3.0
hast-util-heading-rank: 3.0.0
hast-util-is-element: 3.0.0
unified: 11.0.5
unist-util-visit: 5.0.0
rehype-ignore@2.0.3:
dependencies:
hast-util-select: 6.0.4
unified: 11.0.5
unist-util-visit: 5.0.0
rehype-parse@9.0.1:
dependencies:
'@types/hast': 3.0.4
hast-util-from-html: 2.0.3
unified: 11.0.5
rehype-prism-plus@2.0.0:
dependencies:
hast-util-to-string: 3.0.1
parse-numeric-range: 1.3.0
refractor: 4.9.0
rehype-parse: 9.0.1
unist-util-filter: 5.0.1
unist-util-visit: 5.0.0
rehype-prism-plus@2.0.1:
dependencies:
hast-util-to-string: 3.0.1
parse-numeric-range: 1.3.0
refractor: 4.9.0
rehype-parse: 9.0.1
unist-util-filter: 5.0.1
unist-util-visit: 5.0.0
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw: 9.1.0
vfile: 6.0.3
rehype-rewrite@4.0.4:
dependencies:
hast-util-select: 6.0.4
unified: 11.0.5
unist-util-visit: 5.0.0
rehype-slug@6.0.0:
dependencies:
'@types/hast': 3.0.4
github-slugger: 2.0.0
hast-util-heading-rank: 3.0.0
hast-util-to-string: 3.0.1
unist-util-visit: 5.0.0
rehype-stringify@10.0.1:
dependencies:
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
unified: 11.0.5
rehype@13.0.2:
dependencies:
'@types/hast': 3.0.4
rehype-parse: 9.0.1
rehype-stringify: 10.0.1
unified: 11.0.5
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
mdast-util-gfm: 3.1.0
micromark-extension-gfm: 3.0.0
remark-parse: 11.0.0
remark-stringify: 11.0.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-github-blockquote-alert@1.3.1:
dependencies:
unist-util-visit: 5.0.0
remark-parse@11.0.0: remark-parse@11.0.0:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
@@ -7471,6 +7975,12 @@ snapshots:
unified: 11.0.5 unified: 11.0.5
vfile: 6.0.3 vfile: 6.0.3
remark-stringify@11.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
require-directory@2.1.1: {} require-directory@2.1.1: {}
require-main-filename@2.0.0: {} require-main-filename@2.0.0: {}
@@ -7639,6 +8149,8 @@ snapshots:
dependencies: dependencies:
'@pkgr/core': 0.2.9 '@pkgr/core': 0.2.9
tagged-tag@1.0.0: {}
tailwind-merge@3.4.0: {} tailwind-merge@3.4.0: {}
tailwindcss@4.1.18: {} tailwindcss@4.1.18: {}
@@ -7699,6 +8211,10 @@ snapshots:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-fest@5.4.1:
dependencies:
tagged-tag: 1.0.0
typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
@@ -7726,6 +8242,12 @@ snapshots:
trough: 2.2.0 trough: 2.2.0
vfile: 6.0.3 vfile: 6.0.3
unist-util-filter@5.0.1:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
unist-util-is@6.0.1: unist-util-is@6.0.1:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@@ -7798,6 +8320,11 @@ snapshots:
- '@types/react' - '@types/react'
- '@types/react-dom' - '@types/react-dom'
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
vfile: 6.0.3
vfile-message@4.0.3: vfile-message@4.0.3:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@@ -7864,6 +8391,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
web-namespaces@2.0.1: {}
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
which-module@2.0.1: {} which-module@2.0.1: {}

View File

@@ -19,18 +19,25 @@ import {
import { import {
Input, Input,
} from '@/components/ui/input'; } from '@/components/ui/input';
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo';
const formSchema = z.object({ const formSchema = z.object({
email: z.string(), username: z.string().min(5),
nickname: z.string().min(1), nickname: z.string().min(1),
subtitle: z.string().min(1), subtitle: z.string().min(1),
avatar: z.url().min(1),
}); });
export function EditProfileDialog() { export function EditProfileDialog() {
const { data: user } = useUserInfo();
const { mutateAsync } = useUpdateUser();
const form = useForm({ const form = useForm({
defaultValues: { defaultValues: {
email: '', avatar: user.avatar,
nickname: '', username: user.username,
subtitle: '', nickname: user.nickname,
subtitle: user.subtitle,
}, },
validators: { validators: {
onBlur: formSchema, onBlur: formSchema,
@@ -39,13 +46,12 @@ export function EditProfileDialog() {
value, value,
}) => { }) => {
try { try {
toast( await mutateAsync(value);
<code className="text-white">{JSON.stringify(value, null, 2)}</code>, toast.success('个人资料更新成功');
);
} }
catch (error) { catch (error) {
console.error('Form submission error', error); console.error('Form submission error', error);
toast.error('Failed to submit the form. Please try again.'); toast.error('更新个人资料失败,请重试');
} }
}, },
}); });
@@ -53,60 +59,94 @@ export function EditProfileDialog() {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="w-full mt-4" size="lg"></Button> <Button variant="outline" className="w-full" size="lg"></Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
void form.handleSubmit(); void form.handleSubmit();
}} }}
className="grid gap-4"
> >
<DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
</DialogHeader> </DialogHeader>
<form.Field name="username">
{field => (
<Field> <Field>
<FieldLabel htmlFor="email">Email</FieldLabel> <FieldLabel htmlFor="username"></FieldLabel>
<Input <Input
id="email" id="username"
name="email" name="username"
placeholder="noa@requiem.garden" placeholder={user.username}
value={form.getFieldValue('email')} value={field.state.value}
onChange={e => form.setFieldValue('email', e.target.value)} onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/> />
<FieldError /> <FieldError errors={field.state.meta.errors} />
</Field> </Field>
)}
</form.Field>
<form.Field name="nickname">
{field => (
<Field> <Field>
<FieldLabel htmlFor="nickname"></FieldLabel> <FieldLabel htmlFor="nickname"></FieldLabel>
<Input <Input
id="nickname" id="nickname"
name="nickname" name="nickname"
placeholder="Noa Virellia" placeholder={user.nickname}
value={form.getFieldValue('nickname')} value={field.state.value}
onChange={e => form.setFieldValue('nickname', e.target.value)} onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/> />
<FieldError /> <FieldError errors={field.state.meta.errors} />
</Field> </Field>
)}
</form.Field>
<form.Field name="subtitle">
{field => (
<Field> <Field>
<FieldLabel htmlFor="subtitle"></FieldLabel> <FieldLabel htmlFor="subtitle"></FieldLabel>
<Input <Input
id="subtitle" id="subtitle"
name="subtitle" name="subtitle"
placeholder="天生骄傲" placeholder={user.subtitle}
value={form.getFieldValue('subtitle')} value={field.state.value}
onChange={e => form.setFieldValue('subtitle', e.target.value)} onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/> />
<FieldError /> <FieldError errors={field.state.meta.errors} />
</Field> </Field>
)}
</form.Field>
<form.Field name="avatar">
{field => (
<Field>
<FieldLabel htmlFor="avatar"></FieldLabel>
<Input
id="avatar"
name="avatar"
placeholder={user.avatar}
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.Field>
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button variant="outline"></Button> <Button variant="outline"></Button>
</DialogClose> </DialogClose>
<DialogClose asChild>
<Button type="submit"></Button> <Button type="submit"></Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent>
</form> </form>
</DialogContent>
</Dialog> </Dialog>
); );
} }

View File

@@ -1,102 +0,0 @@
import {
useForm,
} from '@tanstack/react-form';
import {
toast,
} from 'sonner';
import {
z,
} from 'zod';
import {
Button,
} from '@/components/ui/button';
import {
Field,
FieldError,
FieldLabel,
} from '@/components/ui/field';
import {
Input,
} from '@/components/ui/input';
const formSchema = z.object({
email: z.string(),
nickname: z.string().min(1),
subtitle: z.string().min(1),
});
export default function EditProfileForm() {
const form = useForm({
defaultValues: {
email: '',
nickname: '',
subtitle: '',
},
validators: {
onBlur: formSchema,
},
onSubmit: async ({
value,
}) => {
try {
toast(
<code className="text-white">{JSON.stringify(value, null, 2)}</code>,
);
}
catch (error) {
console.error('Form submission error', error);
toast.error('Failed to submit the form. Please try again.');
}
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}
>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
placeholder="noa@requiem.garden"
value={form.getFieldValue('email')}
onChange={e => form.setFieldValue('email', e.target.value)}
/>
<FieldError />
</Field>
<Field>
<FieldLabel htmlFor="nickname"></FieldLabel>
<Input
id="nickname"
name="nickname"
placeholder="Noa Virellia"
value={form.getFieldValue('nickname')}
onChange={e => form.setFieldValue('nickname', e.target.value)}
/>
<FieldError />
</Field>
<Field>
<FieldLabel htmlFor="subtitle"></FieldLabel>
<Input
id="subtitle"
name="subtitle"
placeholder="天生骄傲"
value={form.getFieldValue('subtitle')}
onChange={e => form.setFieldValue('subtitle', e.target.value)}
/>
<FieldError />
</Field>
<Button type="submit"></Button>
</form>
);
}

View File

@@ -1,34 +1,81 @@
import { Mail } from 'lucide-react'; import MDEditor from '@uiw/react-md-editor';
import { isNil } from 'lodash-es';
import { Mail, Pencil } from 'lucide-react';
import { useState } from 'react';
import Markdown from 'react-markdown'; import Markdown from 'react-markdown';
import { toast } from 'sonner';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo'; import { useUserInfo } from '@/hooks/data/useUserInfo';
import { base64ToUtf8 } from '@/lib/utils'; import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
import { Button } from '../ui/button';
import { EditProfileDialog } from './edit-profile-dialog'; import { EditProfileDialog } from './edit-profile-dialog';
export function MainProfile() { export function MainProfile() {
const { data: user } = useUserInfo(); const { data: user } = useUserInfo();
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio));
const [enableBioEdit, setEnableBioEdit] = useState(false);
const { mutateAsync } = useUpdateUser();
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
<div className="flex w-full flex-row gap-4 mt-2"> <div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
<Avatar className="size-16 rounded-full border-2 border-muted"> <div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Avatar className="size-16 rounded-full border-2 border-muted lg:size-64">
<AvatarImage src={user.avatar} alt={user.nickname} /> <AvatarImage src={user.avatar} alt={user.nickname} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback> <AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-1 flex-col justify-center"> <div className="flex flex-1 flex-col justify-center lg:mt-3">
<span className="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span> <span className="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span>
<span className="text-[20px] text-muted-foreground" aria-hidden="true">{user.subtitle}</span> <span className="text-[20px] text-muted-foreground" aria-hidden="true">{user.subtitle}</span>
</div> </div>
</div> </div>
<EditProfileDialog /> <div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
<section className="px-2 mt-4">
<div className="flex flex-row gap-2 items-center text-sm">
<Mail className="h-4 w-4 stroke-muted-foreground" /> <Mail className="h-4 w-4 stroke-muted-foreground" />
{user.email} {user.email}
</div> </div>
</section> </div>
<section className="rounded-md border border-muted w-full min-h-72 mt-4 p-6 prose dark:prose-invert max-w-[1012px] self-center"> <EditProfileDialog />
</div>
</div>
<section className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
{/* Bio */} {/* Bio */}
<Markdown>{base64ToUtf8(user.bio)}</Markdown> {enableBioEdit
? (
<MDEditor
value={bio}
onChange={setBio}
height="100%"
/>
)
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
<Button
className="absolute bottom-4 right-4"
// eslint-disable-next-line ts/no-misused-promises
onClick={async () => {
if (!enableBioEdit) {
setEnableBioEdit(true);
}
else {
if (!isNil(bio)) {
try {
await mutateAsync({ bio: utf8ToBase64(bio) });
setEnableBioEdit(false);
}
catch (error) {
console.error(error);
toast.error('个人简介更新失败');
}
}
}
}}
size="icon-sm"
variant={enableBioEdit ? 'default' : 'outline'}
>
<Pencil />
</Button>
</section> </section>
</div> </div>
); );

View File

@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { axiosClient } from '@/lib/axios';
interface UpdateUserPayload {
avatar?: string;
bio?: string;
nickname?: string;
subtitle?: string;
username?: string;
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: UpdateUserPayload) => {
return axiosClient.patch<{ status: string }>('/user/update', payload);
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['userInfo'] });
},
});
}

View File

@@ -6,6 +6,7 @@ export function useUserInfo() {
queryKey: ['userInfo'], queryKey: ['userInfo'],
queryFn: async () => { queryFn: async () => {
const response = await axiosClient.get<{ const response = await axiosClient.get<{
username: string;
user_id: string; user_id: string;
email: string; email: string;
type: string; type: string;

View File

@@ -1,10 +1,17 @@
import type { AxiosRequestConfig } from 'axios'; import type { AxiosError, AxiosRequestConfig } from 'axios';
import axios, { AxiosError } from 'axios'; import type { JsonValue } from 'type-fest';
import axios from 'axios';
import { isNil } from 'lodash-es';
import { router } from '@/lib/router'; import { router } from '@/lib/router';
import { doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token'; import { clearTokens, doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token';
export const HEADER_API_VERSION = {
'X-Api-Version': 'latest',
};
export const axiosClient = axios.create({ export const axiosClient = axios.create({
baseURL: '/api/v1/', baseURL: '/api/v1/',
headers: HEADER_API_VERSION,
}); });
axiosClient.interceptors.request.use((config) => { axiosClient.interceptors.request.use((config) => {
@@ -18,27 +25,43 @@ axiosClient.interceptors.request.use((config) => {
type RetryConfig = AxiosRequestConfig & { _retry?: boolean }; type RetryConfig = AxiosRequestConfig & { _retry?: boolean };
axiosClient.interceptors.response.use(undefined, async (error: AxiosError) => { interface ResponseData {
code: number;
error_id: string;
status: string;
data: JsonValue;
}
axiosClient.interceptors.response.use(async (response) => {
const data = response.data as ResponseData;
if (data.code !== 200) {
return Promise.reject(data);
}
response.data = data.data;
return response;
}, async (error: AxiosError) => {
const originalRequest = error.config as RetryConfig | undefined; const originalRequest = error.config as RetryConfig | undefined;
if (!error.response || error.response.status !== 401 || !originalRequest) { if (!error.response || error.response.status !== 401 || !originalRequest) {
return Promise.reject(error); return Promise.reject(error);
} }
if (error.response.status === 401 && originalRequest.url !== '/auth/refresh' && !isNil(getRefreshToken())) {
if (error.response.status === 401 && getRefreshToken() !== null) {
try { try {
const maybeRefreshTokenValue = await doRefreshToken(); const maybeRefreshTokenResponse = await doRefreshToken();
const { access_token, refresh_token } = maybeRefreshTokenValue.data; if (maybeRefreshTokenResponse.status !== 200) {
throw new Error('Failed to refresh token');
}
const { access_token, refresh_token } = maybeRefreshTokenResponse.data;
originalRequest.headers = originalRequest.headers ?? {}; originalRequest.headers = originalRequest.headers ?? {};
originalRequest.headers.Authorization = `Bearer ${access_token}`; originalRequest.headers.Authorization = `Bearer ${access_token}`;
setToken(access_token); setToken(access_token);
setRefreshToken(refresh_token); setRefreshToken(refresh_token);
return await axiosClient(originalRequest); return await axiosClient(originalRequest);
} }
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) { catch (e) {
if (e instanceof AxiosError && e.status === 401) { // Should remove token (tokens are out of date)
clearTokens();
await router.navigate({ to: '/authorize' }); await router.navigate({ to: '/authorize' });
return Promise.reject(error);
}
} }
} }
}); });

View File

@@ -1,4 +1,4 @@
import axios from 'axios'; import { axiosClient, HEADER_API_VERSION } from './axios';
export function setToken(token: string) { export function setToken(token: string) {
localStorage.setItem('token', token); localStorage.setItem('token', token);
@@ -30,11 +30,17 @@ export function clearTokens() {
} }
export async function doSetTokenByCode(code: string) { export async function doSetTokenByCode(code: string) {
const { data } = await axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/token', { code }); return new Promise<void>((resolve, reject) => {
axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION }).then(({ data }) => {
setToken(data.access_token); setToken(data.access_token);
setRefreshToken(data.refresh_token); setRefreshToken(data.refresh_token);
resolve();
}).catch((error) => {
reject(error);
});
});
} }
export async function doRefreshToken() { export async function doRefreshToken() {
return axios.post<{ access_token: string; refresh_token: string }>('/api/v1/auth/refresh', { refresh_token: getRefreshToken() }); return axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token: getRefreshToken() }, { headers: HEADER_API_VERSION });
} }

View File

@@ -7,7 +7,7 @@ export const Route = createFileRoute('/_sidebarLayout/profile')({
function RouteComponent() { function RouteComponent() {
return ( return (
<div className="flex min-h-[560px] flex-col gap-6 px-4 py-6"> <div className="flex h-full flex-col gap-6 px-4 py-6">
<MainProfile /> <MainProfile />
</div> </div>
); );

View File

@@ -1,7 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter'; import { zodValidator } from '@tanstack/zod-adapter';
import { isNil } from 'lodash-es';
import z from 'zod'; import z from 'zod';
import { LoginForm } from '@/components/login-form'; import { LoginForm } from '@/components/login-form';
import { axiosClient } from '@/lib/axios';
import { generateOAuthState } from '@/lib/random'; import { generateOAuthState } from '@/lib/random';
import { getToken } from '@/lib/token'; import { getToken } from '@/lib/token';
@@ -22,15 +24,21 @@ export const Route = createFileRoute('/authorize')({
function RouteComponent() { function RouteComponent() {
const token = getToken(); const token = getToken();
const oauthParams = Route.useSearch(); const oauthParams = Route.useSearch();
if (token !== null) { /**
const base = new URL(window.location.origin); * Auth by Token Flow
const url = new URL('/api/v1/auth/redirect', base); */
url.searchParams.set('client_id', oauthParams.client_id); if (!isNil(token)) {
url.searchParams.set('response_type', oauthParams.response_type); axiosClient.post<{ redirect_uri: string }>('/auth/exchange', {
url.searchParams.set('redirect_uri', oauthParams.redirect_uri); client_id: oauthParams.client_id,
url.searchParams.set('state', oauthParams.state); redirect_uri: oauthParams.redirect_uri,
window.location.href = url.toString(); state: oauthParams.state,
return null; }).then((res) => {
window.location.href = res.data.redirect_uri;
}).catch((e) => {
console.error(e);
return 'Token exchange failed';
});
return 'Redirecting';
} }
return ( return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"> <div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">

View File

@@ -1,5 +1,5 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import z from 'zod'; import z from 'zod';
import { doSetTokenByCode } from '@/lib/token'; import { doSetTokenByCode } from '@/lib/token';
@@ -16,10 +16,15 @@ function RouteComponent() {
const { code } = Route.useSearch(); const { code } = Route.useSearch();
const [status, setStatus] = useState('Loading...'); const [status, setStatus] = useState('Loading...');
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
doSetTokenByCode(code).then(() => { doSetTokenByCode(code).then(() => {
void navigate({ to: '/' }); void navigate({ to: '/' });
}).catch((_) => { }).catch((_) => {
setStatus('Error getting token'); setStatus('Error getting token');
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div>{status}</div>; return <div>{status}</div>;
} }

View File

@@ -1,15 +1,15 @@
import path from "node:path"; import path from 'node:path';
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from '@tailwindcss/vite';
import { tanstackRouter } from "@tanstack/router-plugin/vite"; import { tanstackRouter } from '@tanstack/router-plugin/vite';
import react from "@vitejs/plugin-react"; import react from '@vitejs/plugin-react';
import { defineConfig } from "vite"; import { defineConfig } from 'vite';
import svgr from "vite-plugin-svgr"; import svgr from 'vite-plugin-svgr';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tanstackRouter({ tanstackRouter({
target: "react", target: 'react',
autoCodeSplitting: true, autoCodeSplitting: true,
}), }),
react(), react(),
@@ -18,15 +18,15 @@ export default defineConfig({
], ],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), '@': path.resolve(__dirname, './src'),
}, },
}, },
server: { server: {
proxy: { proxy: {
"/api": "http://10.0.0.250:8000", '/api': 'http://10.0.0.10:8000',
}, },
host: "0.0.0.0", host: '0.0.0.0',
port: 5173, port: 5173,
allowedHosts: ["nix.org.cn", "nixos.party"], allowedHosts: ['nix.org.cn', 'nixos.party'],
}, },
}); });

View File

@@ -0,0 +1,11 @@
common:
success: "00000"
error:
invalid_input: "00001"
unauthorized: "00002"
internal: "00003"
permission_denied: "00004"
uuid_parse_failed: "00005"
database: "00006"
missing_user_id: "00007"
user_not_found: "00008"

View File

@@ -0,0 +1,23 @@
endpoint:
auth:
service:
redirect: "01"
magic: "02"
token: "03"
refresh: "04"
exchange: "05"
event:
service:
info: "01"
checkin: "02"
checkin_query: "03"
checkin_submit: "04"
user:
service:
info: "01"
update: "02"
list: "03"
full: "04"
create: "05"
middleware:
service: "01"

View File

@@ -0,0 +1,6 @@
middleware:
service:
gin_logger: "901"
jwt: "902"
permission: "903"
api_version: "904"

View File

@@ -0,0 +1,4 @@
service:
auth: "001"
user: "002"
event: "003"

View File

@@ -0,0 +1,34 @@
api:
version:
not_found: "00001"
auth:
redirect:
token_invalid: "00001"
client_not_found: "00002"
uri_mismatch: "00003"
invalid_uri: "00004"
magic:
turnstile_failed: "00001"
code_gen_failed: "00002"
invalid_external_url: "00003"
invalid_email_config: "00004"
token:
invalid_token: "00001"
gen_failed: "00002"
refresh:
invalid_token: "00001"
renew_failed: "00002"
exchange:
get_user_id_failed: "00001"
code_gen_failed: "00002"
invalid_redirect_uri: "00003"
user:
list:
meilisearch_failed: "00001"
event:
info:
not_found: "00001"
checkin:
gen_code_failed: "00001"
checkin_query:
record_not_found: "00001"

View File

@@ -0,0 +1,5 @@
status:
success: "2"
user: "4"
server: "5"
client: "6"

View File

@@ -0,0 +1,3 @@
type:
common: "1"
specific: "0"

View File

@@ -0,0 +1,8 @@
// Code generated by gen-exception; DO NOT EDIT.
package exception
const (
{{- range .Items }}
{{ .Name }} = "{{ .Value }}"
{{- end }}
)

95
cmd/gen_exception/main.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
type ErrorItem struct {
Name string
Value string
}
type TplData struct {
Items []ErrorItem
}
func toCamel(s string) string {
caser := cases.Title(language.English)
s = strings.ReplaceAll(s, "-", "_")
parts := strings.Split(s, "_")
for i := range parts {
parts[i] = caser.String(parts[i])
}
return strings.Join(parts, "")
}
func recursiveParse(prefix string, raw any, items *[]ErrorItem) {
switch v := raw.(type) {
case map[string]any:
for key, val := range v {
recursiveParse(prefix+toCamel(key), val, items)
}
case string:
*items = append(*items, ErrorItem{
Name: prefix,
Value: v,
})
case int, int64:
*items = append(*items, ErrorItem{
Name: prefix,
Value: fmt.Sprintf("%v", v),
})
}
}
func main() {
yamlDir := "cmd/gen_exception/definitions"
outputDir := "internal/exception"
tplPath := "cmd/gen_exception/exception.tmpl"
if _, err := os.Stat(tplPath); os.IsNotExist(err) {
log.Fatalf("Cannot found tmpl %s", tplPath)
}
funcMap := template.FuncMap{"ToCamel": toCamel}
tmpl := template.Must(template.New("exception.tmpl").Funcs(funcMap).ParseFiles(tplPath))
os.MkdirAll(outputDir, 0755)
files, _ := filepath.Glob(filepath.Join(yamlDir, "*.yaml"))
for _, yamlFile := range files {
content, err := os.ReadFile(yamlFile)
if err != nil {
log.Printf("Read file error: %v", err)
continue
}
var rawData any
if err := yaml.Unmarshal(content, &rawData); err != nil {
log.Printf("Unmarshal error in %s: %v", yamlFile, err)
continue
}
var items []ErrorItem
recursiveParse("", rawData, &items)
baseName := strings.TrimSuffix(filepath.Base(yamlFile), filepath.Ext(yamlFile))
outputFileName := baseName + "_gen.go"
outputPath := filepath.Join(outputDir, outputFileName)
f, _ := os.Create(outputPath)
tmpl.Execute(f, TplData{Items: items})
f.Close()
fmt.Printf("Generated: %s (%d constants)\n", outputPath, len(items))
}
}

View File

@@ -3,22 +3,26 @@ server:
address: :8000 address: :8000
external_url: https://example.com external_url: https://example.com
debug_mode: false debug_mode: false
file_logger: false log_level: debug
service_name: nixcn-cms-backend
database: database:
type: postgres type: postgres
host: 127.0.0.1 host: 127.0.0.1
name: postgres name: postgres
username: postgres username: postgres
password: postgres password: postgres
service_name: nixcn-cms-postgres
cache: cache:
hosts: ["127.0.0.1:6379"] hosts: ["127.0.0.1:6379"]
master: "" master: ""
username: "" username: ""
password: "" password: ""
db: 0 db: 0
service_name: nixcn-cms-redis
search: search:
host: 127.0.0.1 host: 127.0.0.1
api_key: "" api_key: ""
service_name: nixcn-cms-meilisearch
email: email:
host: host:
port: port:
@@ -26,13 +30,6 @@ email:
password: password:
security: security:
insecure_skip_verify: insecure_skip_verify:
from:
auth:
oauth2:
tenant_id:
client_id:
client_secret:
scope:
secrets: secrets:
turnstile_secret: example turnstile_secret: example
client_secret_key: aes_32_byte_string client_secret_key: aes_32_byte_string
@@ -45,3 +42,5 @@ ttl:
kyc: kyc:
ali_access_key_id: example ali_access_key_id: example
ali_access_key_secret: example ali_access_key_secret: example
tracer:
otel_controller_endpoint: localhost:4317

View File

@@ -29,9 +29,9 @@ func Init() {
conf := &config{} conf := &config{}
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
// Dont generate config when using dev mode // Dont generate config when using dev mode
log.Fatalln("Can't read config!") log.Fatalln("[Config] Can't read config!")
} }
if err := viper.Unmarshal(conf); err != nil { if err := viper.Unmarshal(conf); err != nil {
log.Fatal(err) log.Fatalln("[Condig] Can't unmarshal config!")
} }
} }

View File

@@ -9,6 +9,7 @@ type config struct {
Secrets secrets `yaml:"secrets"` Secrets secrets `yaml:"secrets"`
TTL ttl `yaml:"ttl"` TTL ttl `yaml:"ttl"`
KYC kyc `yaml:"kyc"` KYC kyc `yaml:"kyc"`
Tracer tracer `yaml:"tracer"`
} }
type server struct { type server struct {
@@ -16,7 +17,8 @@ type server struct {
Address string `yaml:"address"` Address string `yaml:"address"`
ExternalUrl string `yaml:"external_url"` ExternalUrl string `yaml:"external_url"`
DebugMode string `yaml:"debug_mode"` DebugMode string `yaml:"debug_mode"`
FileLogger string `yaml:"file_logger"` LogLevel string `yaml:"log_level"`
ServiceName string `yaml:"service_name"`
} }
type database struct { type database struct {
@@ -25,6 +27,7 @@ type database struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
ServiceName string `yaml:"service_name"`
} }
type cache struct { type cache struct {
@@ -33,18 +36,13 @@ type cache struct {
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"passowrd"` Password string `yaml:"passowrd"`
DB int `yaml:"db"` DB int `yaml:"db"`
ServiceName string `yaml:"service_name"`
} }
type search struct { type search struct {
Host string `yaml:"host"` Host string `yaml:"host"`
ApiKey string `yaml:"api_key"` ApiKey string `yaml:"api_key"`
} ServiceName string `yaml:"service_name"`
type _email_oauth2 struct {
Tenantid string `yaml:"tenant_id"`
ClientId string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
Scope string `yaml:"scope"`
} }
type email struct { type email struct {
@@ -54,9 +52,6 @@ type email struct {
Password string `yaml:"password"` Password string `yaml:"password"`
Security string `yaml:"security"` Security string `yaml:"security"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify"` InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
From string `yaml:"from"`
Auth string `yaml:"auth"`
Oauth2 _email_oauth2 `yaml:"oauth2"`
} }
type secrets struct { type secrets struct {
@@ -76,3 +71,7 @@ type kyc struct {
AliAccessKeyId string `yaml:"ali_access_key_id"` AliAccessKeyId string `yaml:"ali_access_key_id"`
AliAccessKeySecret string `yaml:"ali_access_key_secret"` AliAccessKeySecret string `yaml:"ali_access_key_secret"`
} }
type tracer struct {
OtelControllerEndpoint string `yaml:"otel_controller_endpoint"`
}

View File

@@ -34,10 +34,10 @@ type AttendanceSearchDoc struct {
CheckinAt time.Time `json:"checkin_at"` CheckinAt time.Time `json:"checkin_at"`
} }
func (self *Attendance) GetAttendance(userId, eventId uuid.UUID) (*Attendance, error) { func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) {
var checkin Attendance var checkin Attendance
err := Database. err := Database.WithContext(ctx).
Where("user_id = ? AND event_id = ?", userId, eventId). Where("user_id = ? AND event_id = ?", userId, eventId).
First(&checkin).Error First(&checkin).Error
@@ -57,10 +57,10 @@ type AttendanceUsers struct {
CheckinAt time.Time `json:"checkin_at"` CheckinAt time.Time `json:"checkin_at"`
} }
func (self *Attendance) GetUsersByEventID(eventID uuid.UUID) (*[]AttendanceUsers, error) { func (self *Attendance) GetUsersByEventID(ctx context.Context, eventID uuid.UUID) (*[]AttendanceUsers, error) {
var result []AttendanceUsers var result []AttendanceUsers
err := Database. err := Database.WithContext(ctx).
Model(&Attendance{}). Model(&Attendance{}).
Select("user_id, checkin_at"). Select("user_id, checkin_at").
Where("event_id = ?", eventID). Where("event_id = ?", eventID).
@@ -75,10 +75,10 @@ type AttendanceEvent struct {
CheckinAt time.Time `json:"checkin_at"` CheckinAt time.Time `json:"checkin_at"`
} }
func (self *Attendance) GetEventsByUserID(userID uuid.UUID) (*[]AttendanceEvent, error) { func (self *Attendance) GetEventsByUserID(ctx context.Context, userID uuid.UUID) (*[]AttendanceEvent, error) {
var result []AttendanceEvent var result []AttendanceEvent
err := Database. err := Database.WithContext(ctx).
Model(&Attendance{}). Model(&Attendance{}).
Select("event_id, checkin_at"). Select("event_id, checkin_at").
Where("user_id = ?", userID). Where("user_id = ?", userID).
@@ -88,12 +88,12 @@ func (self *Attendance) GetEventsByUserID(userID uuid.UUID) (*[]AttendanceEvent,
return &result, err return &result, err
} }
func (self *Attendance) Create() error { func (self *Attendance) Create(ctx context.Context) error {
self.UUID = uuid.New() self.UUID = uuid.New()
self.AttendanceId = uuid.New() self.AttendanceId = uuid.New()
// DB transaction for strong consistency // DB transaction for strong consistency
err := Database.Transaction(func(tx *gorm.DB) error { err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&self).Error; err != nil { if err := tx.Create(&self).Error; err != nil {
return err return err
} }
@@ -103,17 +103,17 @@ func (self *Attendance) Create() error {
return err return err
} }
if err := self.UpdateSearchIndex(); err != nil { if err := self.UpdateSearchIndex(ctx); err != nil {
return err return err
} }
return nil return nil
} }
func (self *Attendance) Update(attendanceId uuid.UUID, checkinTime *time.Time) (*Attendance, error) { func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID, checkinTime *time.Time) (*Attendance, error) {
var attendance Attendance var attendance Attendance
err := Database.Transaction(func(tx *gorm.DB) error { err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Lock the row for update // Lock the row for update
if err := tx. if err := tx.
Where("attendance_id = ?", attendanceId). Where("attendance_id = ?", attendanceId).
@@ -148,32 +148,32 @@ func (self *Attendance) Update(attendanceId uuid.UUID, checkinTime *time.Time) (
} }
// Sync to MeiliSearch (eventual consistency) // Sync to MeiliSearch (eventual consistency)
if err := attendance.UpdateSearchIndex(); err != nil { if err := attendance.UpdateSearchIndex(ctx); err != nil {
return nil, err return nil, err
} }
return &attendance, nil return &attendance, nil
} }
func (self *Attendance) SearchUsersByEvent(eventID string) (*meilisearch.SearchResponse, error) { func (self *Attendance) SearchUsersByEvent(ctx context.Context, eventID string) (*meilisearch.SearchResponse, error) {
index := MeiliSearch.Index("attendance") index := MeiliSearch.Index("attendance")
return index.Search("", &meilisearch.SearchRequest{ return index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
Filter: "event_id = \"" + eventID + "\"", Filter: "event_id = \"" + eventID + "\"",
Sort: []string{"checkin_at:asc"}, Sort: []string{"checkin_at:asc"},
}) })
} }
func (self *Attendance) SearchEventsByUser(userID string) (*meilisearch.SearchResponse, error) { func (self *Attendance) SearchEventsByUser(ctx context.Context, userID string) (*meilisearch.SearchResponse, error) {
index := MeiliSearch.Index("attendance") index := MeiliSearch.Index("attendance")
return index.Search("", &meilisearch.SearchRequest{ return index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
Filter: "user_id = \"" + userID + "\"", Filter: "user_id = \"" + userID + "\"",
Sort: []string{"checkin_at:asc"}, Sort: []string{"checkin_at:asc"},
}) })
} }
func (self *Attendance) UpdateSearchIndex() error { func (self *Attendance) UpdateSearchIndex(ctx context.Context) error {
doc := AttendanceSearchDoc{ doc := AttendanceSearchDoc{
AttendanceId: self.AttendanceId.String(), AttendanceId: self.AttendanceId.String(),
EventId: self.EventId.String(), EventId: self.EventId.String(),
@@ -188,21 +188,20 @@ func (self *Attendance) UpdateSearchIndex() error {
PrimaryKey: &primaryKey, PrimaryKey: &primaryKey,
} }
if _, err := index.UpdateDocuments([]AttendanceSearchDoc{doc}, opts); err != nil { if _, err := index.UpdateDocumentsWithContext(ctx, []AttendanceSearchDoc{doc}, opts); err != nil {
return err return err
} }
return nil return nil
} }
func (self *Attendance) DeleteSearchIndex() error { func (self *Attendance) DeleteSearchIndex(ctx context.Context) error {
index := MeiliSearch.Index("attendance") index := MeiliSearch.Index("attendance")
_, err := index.DeleteDocument(self.AttendanceId.String(), nil) _, err := index.DeleteDocumentWithContext(ctx, self.AttendanceId.String(), nil)
return err return err
} }
func (self *Attendance) GenCheckinCode(eventId uuid.UUID) (*string, error) { func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) {
ctx := context.Background()
ttl := viper.GetDuration("ttl.checkin_code_ttl") ttl := viper.GetDuration("ttl.checkin_code_ttl")
rng := rand.New(rand.NewSource(time.Now().UnixNano())) rng := rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -223,18 +222,16 @@ func (self *Attendance) GenCheckinCode(eventId uuid.UUID) (*string, error) {
} }
} }
func (self *Attendance) VerifyCheckinCode(checkinCode string) error { func (self *Attendance) VerifyCheckinCode(ctx context.Context, checkinCode string) error {
ctx := context.Background()
val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result() val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result()
if err != nil { if err != nil {
return errors.New("invalid or expired checkin code") return errors.New("[Attendance Data] invalid or expired checkin code")
} }
// Expected format: user_id:<uuid>:event_id:<uuid> // Expected format: user_id:<uuid>:event_id:<uuid>
parts := strings.Split(val, ":") parts := strings.Split(val, ":")
if len(parts) != 4 { if len(parts) != 4 {
return errors.New("invalid checkin code format") return errors.New("[Attendance Data] invalid checkin code format")
} }
userIdStr := parts[1] userIdStr := parts[1]
@@ -250,13 +247,13 @@ func (self *Attendance) VerifyCheckinCode(checkinCode string) error {
return err return err
} }
attendanceData, err := self.GetAttendance(userId, eventId) attendanceData, err := self.GetAttendance(ctx, userId, eventId)
if err != nil { if err != nil {
return err return err
} }
time := time.Now() time := time.Now()
_, err = self.Update(attendanceData.AttendanceId, &time) _, err = self.Update(ctx, attendanceData.AttendanceId, &time)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,6 +1,7 @@
package data package data
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@@ -22,9 +23,9 @@ type Client struct {
RedirectUri datatypes.JSON `json:"redirect_uri" gorm:"type:json;not null"` RedirectUri datatypes.JSON `json:"redirect_uri" gorm:"type:json;not null"`
} }
func (self *Client) GetClientByClientId(clientId string) (*Client, error) { func (self *Client) GetClientByClientId(ctx context.Context, clientId string) (*Client, error) {
var client Client var client Client
if err := Database. if err := Database.WithContext(ctx).
Where("client_id = ?", clientId). Where("client_id = ?", clientId).
First(&client).Error; err != nil { First(&client).Error; err != nil {
return nil, err return nil, err
@@ -32,10 +33,10 @@ func (self *Client) GetClientByClientId(clientId string) (*Client, error) {
return &client, nil return &client, nil
} }
func (self *Client) GetDecryptedSecret() (string, error) { func (self *Client) GetDecryptedSecret() ([]byte, error) {
secretKey := viper.GetString("secrets.client_secret_key") secretKey := viper.GetString("secrets.client_secret_key")
secret, err := cryptography.AESCBCDecrypt(self.ClientSecret, []byte(secretKey)) secret, err := cryptography.AESCBCDecrypt(self.ClientSecret, []byte(secretKey))
return string(secret), err return secret, err
} }
type ClientParams struct { type ClientParams struct {
@@ -44,7 +45,7 @@ type ClientParams struct {
RedirectUri []string RedirectUri []string
} }
func (self *Client) Create(params *ClientParams) (*Client, error) { func (self *Client) Create(ctx context.Context, params *ClientParams) (*Client, error) {
jsonRedirectUri, err := json.Marshal(params.RedirectUri) jsonRedirectUri, err := json.Marshal(params.RedirectUri)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -69,7 +70,7 @@ func (self *Client) Create(params *ClientParams) (*Client, error) {
RedirectUri: jsonRedirectUri, RedirectUri: jsonRedirectUri,
} }
if err := Database.Create(&client).Error; err != nil { if err := Database.WithContext(ctx).Create(&client).Error; err != nil {
return nil, err return nil, err
} }
@@ -87,5 +88,5 @@ func (self *Client) ValidateRedirectURI(redirectURI string) error {
return nil return nil
} }
} }
return errors.New("redirect uri not match") return errors.New("[Client Data] redirect uri not match")
} }

View File

@@ -1,11 +1,14 @@
package data package data
import ( import (
"context"
"nixcn-cms/data/drivers" "nixcn-cms/data/drivers"
"os"
"log/slog"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -14,7 +17,7 @@ var Database *gorm.DB
var Redis redis.UniversalClient var Redis redis.UniversalClient
var MeiliSearch meilisearch.ServiceManager var MeiliSearch meilisearch.ServiceManager
func Init() { func Init(ctx context.Context) {
// Init database // Init database
dbType := viper.GetString("database.type") dbType := viper.GetString("database.type")
exDSN := drivers.ExternalDSN{ exDSN := drivers.ExternalDSN{
@@ -25,19 +28,22 @@ func Init() {
} }
if dbType != "postgres" { if dbType != "postgres" {
log.Fatal("[Database] Only support postgras db!") slog.ErrorContext(ctx, "[Database] Only support postgras db!")
os.Exit(1)
} }
// Conect to db // Conect to db
db, err := drivers.Postgres(exDSN) db, err := drivers.Postgres(exDSN)
if err != nil { if err != nil {
log.Fatal("[Database] Error connecting to db!") slog.ErrorContext(ctx, "[Database] Error connecting to db!", "err", err)
os.Exit(1)
} }
// Auto migrate // Auto migrate
err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{}) err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{})
if err != nil { if err != nil {
log.Error("[Database] Error migrating database: ", err) slog.ErrorContext(ctx, "[Database] Error migrating database!", "err", err)
os.Exit(1)
} }
Database = db Database = db
@@ -52,7 +58,8 @@ func Init() {
} }
rdb, err := drivers.Redis(rDSN) rdb, err := drivers.Redis(rDSN)
if err != nil { if err != nil {
log.Fatal("[Redis] Error connecting to Redis: ", err) slog.ErrorContext(ctx, "[Redis] Error connecting to Redis!", "err", err)
os.Exit(1)
} }
Redis = rdb Redis = rdb

View File

@@ -1,10 +1,31 @@
package drivers package drivers
import "github.com/meilisearch/meilisearch-go" import (
"fmt"
"net/http"
"github.com/meilisearch/meilisearch-go"
"github.com/spf13/viper"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func MeiliSearch(dsn MeiliDSN) meilisearch.ServiceManager { func MeiliSearch(dsn MeiliDSN) meilisearch.ServiceManager {
serviceName := viper.GetString("search.service_name")
otelTransport := otelhttp.NewTransport(
http.DefaultTransport,
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s", serviceName, r.Method)
}),
)
httpClient := &http.Client{
Transport: otelTransport,
}
return meilisearch.New(dsn.Host, return meilisearch.New(dsn.Host,
meilisearch.WithAPIKey(dsn.ApiKey), meilisearch.WithAPIKey(dsn.ApiKey),
meilisearch.WithCustomClient(httpClient),
meilisearch.WithContentEncoding( meilisearch.WithContentEncoding(
meilisearch.GzipEncoding, meilisearch.GzipEncoding,
meilisearch.BestCompression, meilisearch.BestCompression,

View File

@@ -1,11 +1,16 @@
package drivers package drivers
import ( import (
"log/slog"
"nixcn-cms/config" "nixcn-cms/config"
"nixcn-cms/logger"
"strings" "strings"
"github.com/spf13/viper"
"go.opentelemetry.io/otel/attribute"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/plugin/opentelemetry/tracing"
) )
func SplitHostPort(url string) (host, port string) { func SplitHostPort(url string) (host, port string) {
@@ -17,8 +22,27 @@ func SplitHostPort(url string) (host, port string) {
} }
func Postgres(dsn ExternalDSN) (*gorm.DB, error) { func Postgres(dsn ExternalDSN) (*gorm.DB, error) {
serviceName := viper.GetString("database.service_name")
host, port := SplitHostPort(dsn.Host) host, port := SplitHostPort(dsn.Host)
conn := "host=" + host + " user=" + dsn.Username + " password=" + dsn.Password + " dbname=" + dsn.Name + " port=" + port + " sslmode=disable TimeZone=" + config.TZ() conn := "host=" + host + " user=" + dsn.Username + " password=" + dsn.Password + " dbname=" + dsn.Name + " port=" + port + " sslmode=disable TimeZone=" + config.TZ()
db, err := gorm.Open(postgres.Open(conn), &gorm.Config{})
db, err := gorm.Open(postgres.Open(conn), &gorm.Config{
Logger: logger.GormLogger(),
})
if err != nil {
return nil, err
}
err = db.Use(tracing.NewPlugin(
tracing.WithAttributes(
attribute.String("db.instance", serviceName),
),
))
if err != nil {
slog.Error("[Database] Error starting otel plugin!", "name", serviceName, "err", err)
}
return db, err return db, err
} }

View File

@@ -2,11 +2,18 @@ package drivers
import ( import (
"context" "context"
"log/slog"
"github.com/redis/go-redis/extra/redisotel/v9"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
) )
func Redis(dsn RedisDSN) (redis.UniversalClient, error) { func Redis(dsn RedisDSN) (redis.UniversalClient, error) {
serviceName := viper.GetString("cache.service_name")
// Connect to Redis // Connect to Redis
rdb := redis.NewUniversalClient(&redis.UniversalOptions{ rdb := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: dsn.Hosts, Addrs: dsn.Hosts,
@@ -15,8 +22,23 @@ func Redis(dsn RedisDSN) (redis.UniversalClient, error) {
Password: dsn.Password, Password: dsn.Password,
DB: dsn.DB, DB: dsn.DB,
}) })
attrs := []attribute.KeyValue{
semconv.DBSystemRedis,
attribute.String("db.instance", serviceName),
}
if err := redisotel.InstrumentMetrics(rdb, redisotel.WithAttributes(attrs...)); err != nil {
slog.Error("[Redis] Error starting otel metrics plugin!", "name", serviceName, "err", err)
}
if err := redisotel.InstrumentTracing(rdb, redisotel.WithAttributes(attrs...)); err != nil {
slog.Error("[Redis] Error starting otel tracing plugin!", "name", serviceName, "err", err)
}
// Ping redis
ctx := context.Background() ctx := context.Background()
// Ping Redis
_, err := rdb.Ping(ctx).Result() _, err := rdb.Ping(ctx).Result()
return rdb, err return rdb, err
} }

View File

@@ -1,6 +1,7 @@
package data package data
import ( import (
"context"
"time" "time"
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
@@ -31,10 +32,10 @@ type EventSearchDoc struct {
EndTime time.Time `json:"end_time"` EndTime time.Time `json:"end_time"`
} }
func (self *Event) GetEventById(eventId uuid.UUID) (*Event, error) { func (self *Event) GetEventById(ctx context.Context, eventId uuid.UUID) (*Event, error) {
var event Event var event Event
err := Database. err := Database.WithContext(ctx).
Where("event_id = ?", eventId). Where("event_id = ?", eventId).
First(&event).Error First(&event).Error
@@ -48,9 +49,9 @@ func (self *Event) GetEventById(eventId uuid.UUID) (*Event, error) {
return &event, nil return &event, nil
} }
func (self *Event) UpdateEventById(eventId uuid.UUID) error { func (self *Event) UpdateEventById(ctx context.Context, eventId uuid.UUID) error {
// DB transaction // DB transaction
if err := Database.Transaction(func(tx *gorm.DB) error { if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Update by business key // Update by business key
if err := tx. if err := tx.
Model(&Event{}). Model(&Event{}).
@@ -68,19 +69,19 @@ func (self *Event) UpdateEventById(eventId uuid.UUID) error {
} }
// Sync search index // Sync search index
if err := self.UpdateSearchIndex(); err != nil { if err := self.UpdateSearchIndex(ctx); err != nil {
return err return err
} }
return nil return nil
} }
func (self *Event) Create() error { func (self *Event) Create(ctx context.Context) error {
self.UUID = uuid.New() self.UUID = uuid.New()
self.EventId = uuid.New() self.EventId = uuid.New()
// DB transaction only // DB transaction only
if err := Database.Transaction(func(tx *gorm.DB) error { if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(self).Error; err != nil { if err := tx.Create(self).Error; err != nil {
return err return err
} }
@@ -90,7 +91,7 @@ func (self *Event) Create() error {
} }
// Search index (eventual consistency) // Search index (eventual consistency)
if err := self.UpdateSearchIndex(); err != nil { if err := self.UpdateSearchIndex(ctx); err != nil {
// TODO: async retry / log // TODO: async retry / log
return err return err
} }
@@ -98,20 +99,20 @@ func (self *Event) Create() error {
return nil return nil
} }
func (self *Event) GetFullTable() (*[]Event, error) { func (self *Event) GetFullTable(ctx context.Context) (*[]Event, error) {
var events []Event var events []Event
err := Database.Find(&events).Error err := Database.WithContext(ctx).Find(&events).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &events, err return &events, err
} }
func (self *Event) FastListEvents(limit, offset int64) (*[]EventSearchDoc, error) { func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventSearchDoc, error) {
index := MeiliSearch.Index("event") index := MeiliSearch.Index("event")
// Fast read from MeiliSearch (no DB involved) // Fast read from MeiliSearch (no DB involved)
result, err := index.Search("", &meilisearch.SearchRequest{ result, err := index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
}) })
@@ -127,7 +128,7 @@ func (self *Event) FastListEvents(limit, offset int64) (*[]EventSearchDoc, error
return &list, nil return &list, nil
} }
func (self *Event) UpdateSearchIndex() error { func (self *Event) UpdateSearchIndex(ctx context.Context) error {
doc := EventSearchDoc{ doc := EventSearchDoc{
EventId: self.EventId.String(), EventId: self.EventId.String(),
Name: self.Name, Name: self.Name,
@@ -143,15 +144,15 @@ func (self *Event) UpdateSearchIndex() error {
PrimaryKey: &primaryKey, PrimaryKey: &primaryKey,
} }
if _, err := index.UpdateDocuments([]EventSearchDoc{doc}, opts); err != nil { if _, err := index.UpdateDocumentsWithContext(ctx, []EventSearchDoc{doc}, opts); err != nil {
return err return err
} }
return nil return nil
} }
func (self *Event) DeleteSearchIndex() error { func (self *Event) DeleteSearchIndex(ctx context.Context) error {
index := MeiliSearch.Index("event") index := MeiliSearch.Index("event")
_, err := index.DeleteDocument(self.EventId.String(), nil) _, err := index.DeleteDocumentWithContext(ctx, self.EventId.String(), nil)
return err return err
} }

View File

@@ -1,6 +1,8 @@
package data package data
import ( import (
"context"
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
@@ -31,10 +33,50 @@ type UserSearchDoc struct {
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
} }
func (self *User) GetByEmail(email string) (*User, error) { func (self *User) SetEmail(s string) *User {
self.Email = s
return self
}
func (self *User) SetUsername(s string) *User {
self.Username = s
return self
}
func (self *User) SetNickname(s string) *User {
self.Nickname = s
return self
}
func (self *User) SetSubtitle(s string) *User {
self.Subtitle = s
return self
}
func (self *User) SetAvatar(s string) *User {
self.Avatar = s
return self
}
func (self *User) SetBio(s string) *User {
self.Bio = s
return self
}
func (self *User) SetPermissionLevel(s uint) *User {
self.PermissionLevel = s
return self
}
func (self *User) SetAllowPublic(s bool) *User {
self.AllowPublic = s
return self
}
func (self *User) GetByEmail(ctx context.Context, email *string) (*User, error) {
var user User var user User
err := Database. err := Database.WithContext(ctx).
Where("email = ?", email). Where("email = ?", email).
First(&user).Error First(&user).Error
@@ -48,10 +90,10 @@ func (self *User) GetByEmail(email string) (*User, error) {
return &user, nil return &user, nil
} }
func (self *User) GetByUserId(userId uuid.UUID) (*User, error) { func (self *User) GetByUserId(ctx context.Context, userId *uuid.UUID) (*User, error) {
var user User var user User
err := Database. err := Database.WithContext(ctx).
Where("user_id = ?", userId). Where("user_id = ?", userId).
First(&user).Error First(&user).Error
@@ -65,12 +107,12 @@ func (self *User) GetByUserId(userId uuid.UUID) (*User, error) {
return &user, err return &user, err
} }
func (self *User) Create() error { func (self *User) Create(ctx context.Context) error {
self.UUID = uuid.New() self.UUID = uuid.New()
self.UserId = uuid.New() self.UserId = uuid.New()
// DB transaction only // DB transaction only
if err := Database.Transaction(func(tx *gorm.DB) error { if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(self).Error; err != nil { if err := tx.Create(self).Error; err != nil {
return err return err
} }
@@ -80,7 +122,7 @@ func (self *User) Create() error {
} }
// Search index (eventual consistency) // Search index (eventual consistency)
if err := self.UpdateSearchIndex(); err != nil { if err := self.UpdateSearchIndex(&ctx); err != nil {
// TODO: async retry / log // TODO: async retry / log
return err return err
} }
@@ -88,8 +130,8 @@ func (self *User) Create() error {
return nil return nil
} }
func (self *User) UpdateByUserID(userId uuid.UUID) error { func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID) error {
return Database.Transaction(func(tx *gorm.DB) error { return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&User{}).Where("user_id = ?", userId).Updates(&self).Error; err != nil { if err := tx.Model(&User{}).Where("user_id = ?", userId).Updates(&self).Error; err != nil {
return err return err
} }
@@ -97,22 +139,22 @@ func (self *User) UpdateByUserID(userId uuid.UUID) error {
}) })
} }
func (self *User) GetFullTable() (*[]User, error) { func (self *User) GetFullTable(ctx context.Context) (*[]User, error) {
var users []User var users []User
err := Database.Find(&users).Error err := Database.WithContext(ctx).Find(&users).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &users, nil return &users, nil
} }
func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) { func (self *User) FastListUsers(ctx context.Context, limit, offset *int64) (*[]UserSearchDoc, error) {
index := MeiliSearch.Index("user") index := MeiliSearch.Index("user")
// Fast read from MeiliSearch, no DB involved // Fast read from MeiliSearch, no DB involved
result, err := index.Search("", &meilisearch.SearchRequest{ result, err := index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
Limit: limit, Limit: *limit,
Offset: offset, Offset: *offset,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -126,7 +168,7 @@ func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) {
return &list, nil return &list, nil
} }
func (self *User) UpdateSearchIndex() error { func (self *User) UpdateSearchIndex(ctx *context.Context) error {
doc := UserSearchDoc{ doc := UserSearchDoc{
UserId: self.UserId.String(), UserId: self.UserId.String(),
Email: self.Email, Email: self.Email,
@@ -142,7 +184,8 @@ func (self *User) UpdateSearchIndex() error {
PrimaryKey: &primaryKey, PrimaryKey: &primaryKey,
} }
if _, err := index.UpdateDocuments( if _, err := index.UpdateDocumentsWithContext(
*ctx,
[]UserSearchDoc{doc}, []UserSearchDoc{doc},
opts, opts,
); err != nil { ); err != nil {
@@ -152,8 +195,8 @@ func (self *User) UpdateSearchIndex() error {
return nil return nil
} }
func (self *User) DeleteSearchIndex() error { func (self *User) DeleteSearchIndex(ctx *context.Context) error {
index := MeiliSearch.Index("user") index := MeiliSearch.Index("user")
_, err := index.DeleteDocument(self.UserId.String(), nil) _, err := index.DeleteDocumentWithContext(*ctx, self.UserId.String(), nil)
return err return err
} }

View File

@@ -10,6 +10,7 @@
just just
watchexec watchexec
fvm fvm
podman
]; ];
dotenv = { dotenv = {
@@ -29,12 +30,21 @@
javascript.corepack.enable = true; javascript.corepack.enable = true;
}; };
env.PODMAN_COMPOSE_PROVIDER = "none";
processes = { processes = {
client-cms = { client-cms = {
exec = "pnpm run dev"; exec = "pnpm run dev";
cwd = "./client/cms"; cwd = "./client/cms";
}; };
backend.exec = "just watch-back"; backend.exec = "sleep 30 && just watch-back";
lgtm.exec = ''
podman rm -f lgtm || true
podman run --name lgtm \
-p 3000:3000 -p 4317:4317 -p 4318:4318 \
-e OTEL_METRIC_EXPORT_INTERVAL=5000 \
docker.io/grafana/otel-lgtm:latest
'';
}; };
services = { services = {

3
generate.go Normal file
View File

@@ -0,0 +1,3 @@
package main
//go:generate go run ./cmd/gen_exception/main.go

49
go.mod
View File

@@ -10,22 +10,39 @@ require (
github.com/aliyun/credentials-go v1.4.5 github.com/aliyun/credentials-go v1.4.5
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/go-viper/mapstructure/v2 v2.4.0 github.com/go-viper/mapstructure/v2 v2.4.0
github.com/goccy/go-json v0.10.5
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/meilisearch/meilisearch-go v0.35.0 github.com/meilisearch/meilisearch-go v0.35.0
github.com/redis/go-redis/extra/redisotel/v9 v9.17.2
github.com/redis/go-redis/v9 v9.17.2 github.com/redis/go-redis/v9 v9.17.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
go.opentelemetry.io/otel/log v0.15.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/sdk/log v0.15.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0
golang.org/x/oauth2 v0.34.0 golang.org/x/text v0.33.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
gorm.io/datatypes v1.2.7 gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
gorm.io/plugin/opentelemetry v0.1.16
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.61.5 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
@@ -33,20 +50,27 @@ require (
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.29.0 // indirect github.com/go-playground/validator/v10 v10.29.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect github.com/goccy/go-yaml v1.19.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
@@ -54,15 +78,22 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect github.com/quic-go/quic-go v0.57.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
@@ -71,15 +102,21 @@ require (
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect golang.org/x/arch v0.23.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/driver/mysql v1.5.6 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect
gorm.io/driver/mysql v1.5.7 // indirect
) )

141
go.sum
View File

@@ -2,6 +2,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
@@ -61,6 +65,8 @@ github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPII
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -78,6 +84,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -88,6 +96,15 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -105,6 +122,7 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -124,10 +142,16 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 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.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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.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.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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -136,6 +160,10 @@ 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 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/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -152,8 +180,14 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -177,9 +211,17 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -187,14 +229,20 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 h1:KYWnHK9pwzOUo3sNJlNmzRwZ5mw7opugn8njtGThKNg=
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2/go.mod h1:wsfMQVl/GFYD9Gx/tlxurlTtvHkZRAt8j1qi27eIlTk=
github.com/redis/go-redis/extra/redisotel/v9 v9.17.2 h1:wthFPRW3Y50CknMrjjJoYwXUFR4U7hMVJCMeLzDI8s4=
github.com/redis/go-redis/extra/redisotel/v9 v9.17.2/go.mod h1:iqfQX7U2o8MWSl8W+Ah8KqbQyi/UoR/MQNgvaUyA1wc=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -215,6 +263,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -224,6 +273,7 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@@ -231,11 +281,57 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0 h1:7IKZbAYwlwLXAdu7SVPhzTjDjogWZxP4MIa7rovY+PU=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0/go.mod h1:+TF5nf3NIv2X8PGxqfYOaRnAoMM43rUA2C3XsN2DoWA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -249,6 +345,7 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
@@ -263,6 +360,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -277,7 +375,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -292,13 +392,13 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
@@ -313,9 +413,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -344,6 +444,7 @@ golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.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.3/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@@ -351,8 +452,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -363,6 +464,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/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-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
@@ -370,25 +473,37 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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/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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 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.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -404,8 +519,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/clickhouse v0.7.0 h1:BCrqvgONayvZRgtuA6hdya+eAW5P2QVagV3OlEp1vtA=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/clickhouse v0.7.0/go.mod h1:TmNo0wcVTsD4BBObiRnCahUgHJHjBIwuRejHwYt3JRs=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
@@ -415,5 +532,7 @@ gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOze
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc=
gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -21,12 +21,12 @@ import (
func DecodeB64Json(b64Json string) (*KycInfo, error) { func DecodeB64Json(b64Json string) (*KycInfo, error) {
rawJson, err := base64.StdEncoding.DecodeString(b64Json) rawJson, err := base64.StdEncoding.DecodeString(b64Json)
if err != nil { if err != nil {
return nil, errors.New("invalid base64 json") return nil, errors.New("[KYC] invalid base64 json")
} }
var kyc KycInfo var kyc KycInfo
if err := json.Unmarshal(rawJson, &kyc); err != nil { if err := json.Unmarshal(rawJson, &kyc); err != nil {
return nil, errors.New("invalid json structure") return nil, errors.New("[KYC] invalid json structure")
} }
return &kyc, nil return &kyc, nil
@@ -56,7 +56,7 @@ func DecodeAES(cipherStr string) (*KycInfo, error) {
var kyc KycInfo var kyc KycInfo
if err := json.Unmarshal(plainBytes, &kyc); err != nil { if err := json.Unmarshal(plainBytes, &kyc); err != nil {
return nil, errors.New("invalid decrypted json") return nil, errors.New("[KYC] invalid decrypted json")
} }
return &kyc, nil return &kyc, nil

View File

@@ -0,0 +1,7 @@
package kyc
type KycAli struct {
ParamType string `json:"param_type"`
IdentifyNum string `json:"identify_num"`
UserName string `json:"user_name"`
}

View File

@@ -14,9 +14,7 @@ type Token struct {
Email string Email string
} }
func NewAuthCode(clientId string, email string) (string, error) { func NewAuthCode(ctx context.Context, clientId string, email string) (string, error) {
ctx := context.Background()
// generate random code // generate random code
b := make([]byte, 32) b := make([]byte, 32)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
@@ -48,8 +46,7 @@ func NewAuthCode(clientId string, email string) (string, error) {
return code, nil return code, nil
} }
func VerifyAuthCode(code string) (*Token, bool) { func VerifyAuthCode(ctx context.Context, code string) (*Token, bool) {
ctx := context.Background()
key := "auth_code:" + code key := "auth_code:" + code
// Read auth code payload // Read auth code payload

View File

@@ -39,11 +39,11 @@ func (self *Token) NewClaims(clientId string, userId uuid.UUID) JwtClaims {
} }
// Generate access token // Generate access token
func (self *Token) GenerateAccessToken(clientId string, userId uuid.UUID) (string, error) { func (self *Token) GenerateAccessToken(ctx context.Context, clientId string, userId uuid.UUID) (string, error) {
claims := self.NewClaims(clientId, userId) claims := self.NewClaims(clientId, userId)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
clientData, err := new(data.Client).GetClientByClientId(clientId) clientData, err := new(data.Client).GetClientByClientId(ctx, clientId)
if err != nil { if err != nil {
return "", fmt.Errorf("error getting client data: %v", err) return "", fmt.Errorf("error getting client data: %v", err)
} }
@@ -70,9 +70,9 @@ func (self *Token) GenerateRefreshToken() (string, error) {
} }
// Issue both access and refresh token // Issue both access and refresh token
func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, string, error) { func (self *Token) IssueTokens(ctx context.Context, clientId string, userId uuid.UUID) (string, string, error) {
// access token // access token
access, err := self.GenerateAccessToken(clientId, userId) access, err := self.GenerateAccessToken(ctx, clientId, userId)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@@ -83,7 +83,6 @@ func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, strin
return "", "", err return "", "", err
} }
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl") ttl := viper.GetDuration("ttl.refresh_ttl")
refreshKey := "refresh:" + refresh refreshKey := "refresh:" + refresh
@@ -122,21 +121,20 @@ func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, strin
} }
// Refresh access token // Refresh access token
func (self *Token) RefreshAccessToken(refreshToken string) (string, error) { func (self *Token) RefreshAccessToken(ctx context.Context, refreshToken string) (string, error) {
ctx := context.Background()
key := "refresh:" + refreshToken key := "refresh:" + refreshToken
// read refresh token bind data // read refresh token bind data
dataMap, err := data.Redis.HGetAll(ctx, key).Result() dataMap, err := data.Redis.HGetAll(ctx, key).Result()
if err != nil || len(dataMap) == 0 { if err != nil || len(dataMap) == 0 {
return "", errors.New("invalid refresh token") return "", errors.New("[Auth Token] invalid refresh token")
} }
userIdStr := dataMap["user_id"] userIdStr := dataMap["user_id"]
clientId := dataMap["client_id"] clientId := dataMap["client_id"]
if userIdStr == "" || clientId == "" { if userIdStr == "" || clientId == "" {
return "", errors.New("refresh token corrupted") return "", errors.New("[Auth Token] refresh token corrupted")
} }
userId, err := uuid.Parse(userIdStr) userId, err := uuid.Parse(userIdStr)
@@ -145,11 +143,10 @@ func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
} }
// Generate new access token // Generate new access token
return self.GenerateAccessToken(clientId, userId) return self.GenerateAccessToken(ctx, clientId, userId)
} }
func (self *Token) RenewRefreshToken(refreshToken string) (string, error) { func (self *Token) RenewRefreshToken(ctx context.Context, refreshToken string) (string, error) {
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl") ttl := viper.GetDuration("ttl.refresh_ttl")
oldKey := "refresh:" + refreshToken oldKey := "refresh:" + refreshToken
@@ -157,14 +154,14 @@ func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
// read old refresh token bind data // read old refresh token bind data
dataMap, err := data.Redis.HGetAll(ctx, oldKey).Result() dataMap, err := data.Redis.HGetAll(ctx, oldKey).Result()
if err != nil || len(dataMap) == 0 { if err != nil || len(dataMap) == 0 {
return "", errors.New("invalid refresh token") return "", errors.New("[Auth Token] invalid refresh token")
} }
userIdStr := dataMap["user_id"] userIdStr := dataMap["user_id"]
clientId := dataMap["client_id"] clientId := dataMap["client_id"]
if userIdStr == "" || clientId == "" { if userIdStr == "" || clientId == "" {
return "", errors.New("refresh token corrupted") return "", errors.New("[Auth Token] refresh token corrupted")
} }
// generate new refresh token // generate new refresh token
@@ -174,7 +171,7 @@ func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
} }
// revoke old refresh token // revoke old refresh token
if err := self.RevokeRefreshToken(refreshToken); err != nil { if err := self.RevokeRefreshToken(ctx, refreshToken); err != nil {
return "", err return "", err
} }
@@ -211,9 +208,7 @@ func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
return newRefresh, nil return newRefresh, nil
} }
func (self *Token) RevokeRefreshToken(refreshToken string) error { func (self *Token) RevokeRefreshToken(ctx context.Context, refreshToken string) error {
ctx := context.Background()
refreshKey := "refresh:" + refreshToken refreshKey := "refresh:" + refreshToken
// read refresh token metadata (user_id, client_id) // read refresh token metadata (user_id, client_id)
@@ -246,7 +241,7 @@ func (self *Token) RevokeRefreshToken(refreshToken string) error {
return err return err
} }
func (self *Token) HeaderVerify(header string) (string, error) { func (self *Token) HeaderVerify(ctx context.Context, header string) (string, error) {
if header == "" { if header == "" {
return "", nil return "", nil
} }
@@ -254,7 +249,7 @@ func (self *Token) HeaderVerify(header string) (string, error) {
// Split header to 2 // Split header to 2
parts := strings.SplitN(header, " ", 2) parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" { if len(parts) != 2 || parts[0] != "Bearer" {
return "", errors.New("invalid Authorization header format") return "", errors.New("[Auth Token] invalid Authorization header format")
} }
tokenStr := parts[1] tokenStr := parts[1]
@@ -266,14 +261,14 @@ func (self *Token) HeaderVerify(header string) (string, error) {
claims, claims,
func(token *jwt.Token) (any, error) { func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method") return nil, errors.New("[Auth Token] unexpected signing method")
} }
if claims.ClientId == "" { if claims.ClientId == "" {
return nil, errors.New("client_id missing in token") return nil, errors.New("[Auth Token] client_id missing in token")
} }
clientData, err := new(data.Client).GetClientByClientId(claims.ClientId) clientData, err := new(data.Client).GetClientByClientId(ctx, claims.ClientId)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting client data: %v", err) return nil, fmt.Errorf("error getting client data: %v", err)
} }
@@ -288,7 +283,8 @@ func (self *Token) HeaderVerify(header string) (string, error) {
) )
if err != nil || !token.Valid { if err != nil || !token.Valid {
return "", errors.New("invalid or expired token") fmt.Println(err)
return "", errors.New("[Auth Token] invalid or expired token")
} }
return claims.UserID.String(), nil return claims.UserID.String(), nil

View File

@@ -21,7 +21,7 @@ func normalizeKey(key []byte) ([]byte, error) {
case 16, 24, 32: case 16, 24, 32:
return key, nil return key, nil
default: default:
return nil, errors.New("AES key length must be 16, 24, or 32 bytes") return nil, errors.New("[Cryptography AES] AES key length must be 16, 24, or 32 bytes")
} }
} }
@@ -74,7 +74,7 @@ func AESGCMDecrypt(encoded string, key []byte) ([]byte, error) {
} }
if len(data) < gcm.NonceSize() { if len(data) < gcm.NonceSize() {
return nil, errors.New("ciphertext too short") return nil, errors.New("[Cryptography AES] ciphertext too short")
} }
nonce := data[:gcm.NonceSize()] nonce := data[:gcm.NonceSize()]
@@ -92,11 +92,11 @@ func pkcs7Pad(data []byte, blockSize int) []byte {
func pkcs7Unpad(data []byte) ([]byte, error) { func pkcs7Unpad(data []byte) ([]byte, error) {
length := len(data) length := len(data)
if length == 0 { if length == 0 {
return nil, errors.New("invalid padding") return nil, errors.New("[Cryptography AES] invalid padding")
} }
padding := int(data[length-1]) padding := int(data[length-1])
if padding == 0 || padding > length { if padding == 0 || padding > length {
return nil, errors.New("invalid padding") return nil, errors.New("[Cryptography AES] invalid padding")
} }
return data[:length-padding], nil return data[:length-padding], nil
} }
@@ -143,7 +143,7 @@ func AESCBCDecrypt(encoded string, key []byte) ([]byte, error) {
} }
if len(data) < block.BlockSize() { if len(data) < block.BlockSize() {
return nil, errors.New("ciphertext too short") return nil, errors.New("[Cryptography AES] ciphertext too short")
} }
iv := data[:block.BlockSize()] iv := data[:block.BlockSize()]
@@ -195,7 +195,7 @@ func AESCFBDecrypt(encoded string, key []byte) ([]byte, error) {
} }
if len(data) < block.BlockSize() { if len(data) < block.BlockSize() {
return nil, errors.New("ciphertext too short") return nil, errors.New("[Cryptography AES] ciphertext too short")
} }
iv := data[:block.BlockSize()] iv := data[:block.BlockSize()]

84
internal/email/email.go Normal file
View File

@@ -0,0 +1,84 @@
package email
import (
"crypto/tls"
"errors"
"strings"
"time"
"github.com/spf13/viper"
gomail "gopkg.in/gomail.v2"
)
type Client struct {
dialer *gomail.Dialer
host string
port int
username string
security string
insecure bool
}
func (self *Client) NewSMTPClient() (*Client, error) {
host := viper.GetString("email.host")
port := viper.GetInt("email.port")
user := viper.GetString("email.username")
pass := viper.GetString("email.password")
security := strings.ToLower(viper.GetString("email.security"))
insecure := viper.GetBool("email.insecure_skip_verify")
if host == "" || port == 0 || user == "" {
return nil, errors.New("[Email] SMTP config not set")
}
if pass == "" {
return nil, errors.New("[Email] SMTP basic auth requires email.password")
}
dialer := gomail.NewDialer(host, port, user, pass)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: insecure,
}
switch security {
case "ssl":
dialer.SSL = true
case "starttls":
dialer.SSL = false
case "plain", "":
dialer.SSL = false
dialer.TLSConfig = nil
default:
return nil, errors.New("[Email] unknown smtp security mode: " + security)
}
return &Client{
dialer: dialer,
host: host,
port: port,
username: user,
security: security,
insecure: insecure,
}, nil
}
func (c *Client) Send(from, to, subject, html string) (string, error) {
if c.dialer == nil {
return "", errors.New("[Email] SMTP dialer not initialized")
}
m := gomail.NewMessage()
m.SetHeader("From", from)
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", html)
if err := c.dialer.DialAndSend(m); err != nil {
return "", err
}
return time.Now().Format(time.RFC3339Nano), nil
}

View File

@@ -0,0 +1,76 @@
package exception
import (
"context"
"fmt"
)
// 12 chars len
// :1=status
// :3=service
// :2=endpoint
// :1=common/specific
// :5=original
type Builder struct {
Status string
Service string
Endpoint string
Type string
Original string
Error error
ErrorCode string
}
func (self *Builder) SetStatus(s string) *Builder {
self.Status = s
return self
}
func (self *Builder) SetService(s string) *Builder {
self.Service = s
return self
}
func (self *Builder) SetEndpoint(s string) *Builder {
self.Endpoint = s
return self
}
func (self *Builder) SetType(s string) *Builder {
self.Type = s
return self
}
func (self *Builder) SetOriginal(s string) *Builder {
self.Original = s
return self
}
func (self *Builder) SetError(e error) *Builder {
self.Error = e
return self
}
func (self *Builder) build() {
self.ErrorCode = fmt.Sprintf("%s%s%s%s%s",
self.Status,
self.Service,
self.Endpoint,
self.Type,
self.Original,
)
}
func (self *Builder) Throw(ctx context.Context) *Builder {
self.build()
if self.Error != nil {
ErrorHandler(ctx, self.Status, self.ErrorCode, self.Error)
}
return self
}
func (self *Builder) String() string {
self.build()
return self.ErrorCode
}

View File

@@ -0,0 +1,19 @@
package exception
import (
"context"
"log/slog"
)
func ErrorHandler(ctx context.Context, status string, errorCode string, err error) {
switch status {
case StatusSuccess:
slog.InfoContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
case StatusUser:
slog.WarnContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
case StatusServer:
slog.ErrorContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
case StatusClient:
slog.ErrorContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
}
}

17
internal/kyc/types.go Normal file
View File

@@ -0,0 +1,17 @@
package kyc
type KycInfo struct {
Type string `json:"type"` // cnrid/passport
LegalName string `json:"legal_name"`
ResidentId string `json:"rsident_id"`
PassportInfo PassportInfo `json:"passport_info"`
}
type PassportInfo struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
DateOfExpire string `json:"date_of_expire"`
Nationality string `json:"nationality"`
DocumentType string `json:"document_type"`
DocumentNumber string `json:"document_number"`
}

View File

@@ -0,0 +1,13 @@
package screalid
type KycPassportResponse struct {
Status string `json:"status"`
FinalResult struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
DateOfExpire string `json:"dateOfExpire"`
Nationality string `json:"nationality"`
DocumentType string `json:"documentType"`
DocumentNumber string `json:"documentNumber"`
} `json:"finalResult"`
}

View File

@@ -9,7 +9,9 @@ client_cms_dir := join(client_dir, "cms")
server_exec_path := join(output_dir, project_name) server_exec_path := join(output_dir, project_name)
server_entry := "main.go" server_entry := "main.go"
install: install-cms install: install-cms install-back
generate: gen-back
install-cms: install-cms:
cd {{ client_cms_dir }} && {{ pnpm_cmd }} install cd {{ client_cms_dir }} && {{ pnpm_cmd }} install
@@ -24,20 +26,26 @@ build-client-cms:
build-back: build-back:
{{ go_cmd }} build -o {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }} {{ server_entry }} {{ go_cmd }} build -o {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }} {{ server_entry }}
install-back:
cd {{ project_dir }} && go mod tidy
run-back: run-back:
cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }} cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }}
test-back: test-back:
cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. ./... cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. ./...
gen-back:
cd {{ project_dir }} && go generate .
watch-back: watch-back:
watchexec -r -e go,yaml,tpl -i '.devenv/**' -i '.direnv/**' -i 'client/**' -i 'vendor/**' 'go build -o {{ server_exec_path }} . && cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}' watchexec -r -e go,yaml,tpl -i '.devenv/**' -i '.direnv/**' -i 'client/**' -i 'vendor/**' 'go build -o {{ server_exec_path }} . && cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}'
dev: clean install dev: clean install generate
devenv up --verbose devenv up --verbose
dev-client-cms: install-cms dev-client-cms: install-cms
devenv up client-cms --verbose devenv up client-cms --verbose
dev-back: clean dev-back: clean install-back gen-back
devenv up backend postgres redis meilisearch --verbose devenv up backend postgres redis meilisearch lgtm --verbose

28
logger/gorm.go Normal file
View File

@@ -0,0 +1,28 @@
package logger
import (
"fmt"
"log/slog"
"time"
"gorm.io/gorm/logger"
)
type SlogWriter struct{}
func (w *SlogWriter) Printf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
slog.Info(msg)
}
func GormLogger() logger.Interface {
return logger.New(
&SlogWriter{},
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
)
}

View File

@@ -1,60 +0,0 @@
package logger
import (
"os"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
func Init() {
FileLogger := viper.GetBool("server.file_logger")
DebugMode := viper.GetBool("server.debug_mode")
if FileLogger == true {
gin.DisableConsoleColor()
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
log.Panicln("Error opening log file: ", err)
}
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(file)
log.SetLevel(log.DebugLevel)
} else {
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stdout)
log.SetLevel(log.WarnLevel)
}
if DebugMode == true {
gin.SetMode(gin.DebugMode)
log.SetLevel(log.DebugLevel)
} else {
gin.SetMode(gin.ReleaseMode)
}
}
func Gin() gin.HandlerFunc {
return func(ctx *gin.Context) {
startTime := time.Now()
ctx.Next()
endTime := time.Now()
latencyTime := endTime.Sub(startTime)
reqMethod := ctx.Request.Method
reqUri := ctx.Request.RequestURI
statusCode := ctx.Writer.Status()
clientIP := ctx.ClientIP()
log.WithFields(log.Fields{
"METHOD": reqMethod,
"URI": reqUri,
"STATUS": statusCode,
"LATENCY": latencyTime,
"CLIENT_IP": clientIP,
}).Info("HTTP REQUEST")
ctx.Next()
}
}

97
logger/slog.go Normal file
View File

@@ -0,0 +1,97 @@
package logger
import (
"context"
"io"
"log/slog"
"os"
"strings"
"github.com/spf13/viper"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel/trace"
)
type multiHandler struct {
handlers []slog.Handler
}
func (m *multiHandler) Enabled(ctx context.Context, l slog.Level) bool {
for _, h := range m.handlers {
if h.Enabled(ctx, l) {
return true
}
}
return false
}
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
span := trace.SpanFromContext(ctx)
if span.SpanContext().HasTraceID() {
r.AddAttrs(
slog.String("trace_id", span.SpanContext().TraceID().String()),
slog.String("span_id", span.SpanContext().SpanID().String()),
)
}
for _, h := range m.handlers {
_ = h.Handle(ctx, r)
}
return nil
}
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newHandlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
newHandlers[i] = h.WithAttrs(attrs)
}
return &multiHandler{handlers: newHandlers}
}
func (m *multiHandler) WithGroup(name string) slog.Handler {
newHandlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
newHandlers[i] = h.WithGroup(name)
}
return &multiHandler{handlers: newHandlers}
}
func Init() {
levelStr := strings.ToLower(viper.GetString("server.log_level"))
var level slog.Level
switch levelStr {
case "debug":
level = slog.LevelDebug
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
var writer io.Writer = os.Stdout
if level == slog.LevelDebug {
file, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
writer = io.MultiWriter(os.Stdout, file)
}
localHandler := slog.NewJSONHandler(writer, &slog.HandlerOptions{
Level: level,
AddSource: true,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.String(a.Key, a.Value.Time().Format("2006-01-02 15:04:05"))
}
return a
},
})
otelHandler := otelslog.NewHandler(viper.GetString("server.service_name"))
combinedHandler := &multiHandler{
handlers: []slog.Handler{localHandler, otelHandler},
}
slog.SetDefault(slog.New(combinedHandler))
}

22
main.go
View File

@@ -1,15 +1,31 @@
package main package main
import ( import (
"context"
"log/slog"
"nixcn-cms/config" "nixcn-cms/config"
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/logger" "nixcn-cms/logger"
"nixcn-cms/server" "nixcn-cms/server"
"nixcn-cms/tracer"
"time"
) )
func main() { func main() {
config.Init() config.Init()
logger.Init()
data.Init() // OTEL
server.Start() ctx := context.Background()
shutdown := tracer.Init(ctx)
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := shutdown(ctx); err != nil {
slog.Error("[Main] Tracer shutdown failed!", "err", err)
}
}()
logger.Init()
data.Init(ctx)
server.Start(ctx)
} }

View File

@@ -1,12 +1,25 @@
package middleware package middleware
import "github.com/gin-gonic/gin" import (
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
func ApiVersionCheck() gin.HandlerFunc { func ApiVersionCheck() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
apiVersion := c.GetHeader("X-Api-Version") apiVersion := c.GetHeader("X-Api-Version")
if apiVersion == "" { if apiVersion == "" {
c.Abort() errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.MiddlewareServiceApiVersion).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeSpecific).
SetOriginal(exception.ApiVersionNotFound).
Throw(c).
String()
utils.HttpAbort(c, 400, errorCode)
return return
} }
c.Next() c.Next()

56
middleware/gin_logger.go Normal file
View File

@@ -0,0 +1,56 @@
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log/slog"
"time"
"github.com/gin-gonic/gin"
)
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
var body []byte
if c.Request.Body != nil {
body, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
headerJSON, _ := json.Marshal(c.Request.Header)
startTime := time.Now()
c.Next()
ctx := c.Request.Context()
var errorMessage string
if len(c.Errors) > 0 {
errorMessage = c.Errors.String()
}
fields := []any{
"status", c.Writer.Status(),
"method", c.Request.Method,
"uri", c.Request.RequestURI,
"ip", c.ClientIP(),
"latency", time.Since(startTime).String(),
"user_agent", c.Request.UserAgent(),
"headers", string(headerJSON),
"request_body", string(body),
"errors", errorMessage,
}
status := c.Writer.Status()
if len(c.Errors) > 0 || status >= 500 {
slog.ErrorContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
} else if status >= 400 {
slog.WarnContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
} else {
slog.InfoContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
}
}
}

View File

@@ -1,26 +1,32 @@
package middleware package middleware
import ( import (
"nixcn-cms/pkgs/authtoken" "nixcn-cms/internal/authtoken"
"nixcn-cms/internal/exception"
"nixcn-cms/utils" "nixcn-cms/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func JWTAuth(required bool) gin.HandlerFunc { func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
auth := c.GetHeader("Authorization") auth := c.GetHeader("Authorization")
authtoken := new(authtoken.Token) authtoken := new(authtoken.Token)
uid, err := authtoken.HeaderVerify(auth) uid, err := authtoken.HeaderVerify(c, auth)
if err != nil { if err != nil {
utils.HttpAbort(c, 401, "", "unauthorized") errorCode := new(exception.Builder).
return SetStatus(exception.StatusUser).
} SetService(exception.MiddlewareServiceJwt).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUnauthorized).
SetError(err).
Throw(c).
String()
if required == true && uid == "" { utils.HttpAbort(c, 401, errorCode)
utils.HttpAbort(c, 401, "", "unauthorized")
return return
} }

View File

@@ -2,6 +2,7 @@ package middleware
import ( import (
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils" "nixcn-cms/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -15,19 +16,47 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
if !ok { if !ok {
userIdOrig, ok := c.Get("user_id") userIdOrig, ok := c.Get("user_id")
if !ok || userIdOrig.(string) == "" { if !ok || userIdOrig.(string) == "" {
utils.HttpAbort(c, 401, "", "missing user id") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.MiddlewareServicePermission).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Throw(c).
String()
utils.HttpAbort(c, 401, errorCode)
return return
} }
userId, err := uuid.Parse(userIdOrig.(string)) userId, err := uuid.Parse(userIdOrig.(string))
if err != nil { if err != nil {
utils.HttpAbort(c, 500, "", "error parsing user id") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.MiddlewareServicePermission).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Throw(c).
String()
utils.HttpAbort(c, 500, errorCode)
return return
} }
userData, err := new(data.User).GetByUserId(userId) userData, err := new(data.User).GetByUserId(c, &userId)
if err != nil { if err != nil {
utils.HttpAbort(c, 404, "", "user not found") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.MiddlewareServicePermission).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Throw(c).
String()
utils.HttpAbort(c, 404, errorCode)
return return
} }
@@ -38,7 +67,16 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
} }
if permissionLevel < requiredLevel { if permissionLevel < requiredLevel {
utils.HttpAbort(c, 403, "", "permission denied") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.MiddlewareServicePermission).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorPermissionDenied).
Throw(c).
String()
utils.HttpAbort(c, 403, errorCode)
return return
} }

View File

@@ -1,313 +0,0 @@
package email
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/smtp"
"strings"
"sync"
"time"
"github.com/spf13/viper"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
gomail "gopkg.in/gomail.v2"
)
type Client struct {
// basic smtp
dialer *gomail.Dialer
// shared
from string
host string
port int
username string
security string
insecure bool
// auth mode
authMode string
// oauth2
oauth *oauthTokenProvider
}
type oauthTokenProvider struct {
cfg clientcredentials.Config
mu sync.Mutex
token *oauth2.Token
fetchErr error
}
func (p *oauthTokenProvider) getToken(ctx context.Context) (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.token != nil && p.token.Valid() && time.Until(p.token.Expiry) > 60*time.Second {
return p.token.AccessToken, nil
}
tok, err := p.cfg.Token(ctx)
if err != nil {
p.fetchErr = err
return "", err
}
p.token = tok
p.fetchErr = nil
return tok.AccessToken, nil
}
func NewSMTPClient() (*Client, error) {
host := viper.GetString("email.host")
port := viper.GetInt("email.port")
user := viper.GetString("email.username")
pass := viper.GetString("email.password")
from := viper.GetString("email.from")
security := strings.ToLower(viper.GetString("email.security"))
insecure := viper.GetBool("email.insecure_skip_verify")
authMode := strings.ToLower(viper.GetString("email.auth"))
if authMode == "" {
authMode = "basic"
}
if host == "" || port == 0 || user == "" {
return nil, errors.New("SMTP config not set")
}
c := &Client{
from: from,
host: host,
port: port,
username: user,
security: security,
insecure: insecure,
authMode: authMode,
}
switch authMode {
case "basic":
if pass == "" {
return nil, errors.New("SMTP basic auth requires email.password")
}
dialer := gomail.NewDialer(host, port, user, pass)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: insecure,
}
switch security {
case "ssl":
dialer.SSL = true
case "starttls":
dialer.SSL = false
case "plain", "":
dialer.SSL = false
dialer.TLSConfig = nil
default:
return nil, errors.New("unknown smtp security mode: " + security)
}
c.dialer = dialer
return c, nil
case "oauth2":
if security == "" {
security = "starttls"
c.security = "starttls"
}
if security == "plain" {
return nil, errors.New("oauth2 requires TLS (starttls or ssl); plain is not allowed")
}
tenantID := viper.GetString("email.oauth2.tenant_id")
clientID := viper.GetString("email.oauth2.client_id")
clientSecret := viper.GetString("email.oauth2.client_secret")
scope := viper.GetString("email.oauth2.scope")
if scope == "" {
// Microsoft Learn: client credentials for SMTP uses https://outlook.office365.com/.default :contentReference[oaicite:3]{index=3}
scope = "https://outlook.office365.com/.default"
}
if tenantID == "" || clientID == "" || clientSecret == "" {
return nil, errors.New("oauth2 requires email.oauth2.tenant_id/client_id/client_secret")
}
c.oauth = &oauthTokenProvider{
cfg: clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID),
Scopes: []string{scope},
},
}
return c, nil
default:
return nil, errors.New("unknown email.auth: " + authMode)
}
}
func (c *Client) Send(to, subject, html string) (string, error) {
m := gomail.NewMessage()
m.SetHeader("From", c.from)
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", html)
switch c.authMode {
case "basic":
if c.dialer == nil {
return "", errors.New("basic dialer not initialized")
}
if err := c.dialer.DialAndSend(m); err != nil {
return "", err
}
return time.Now().Format(time.RFC3339Nano), nil
case "oauth2":
if err := c.sendWithXOAUTH2(m, to); err != nil {
return "", err
}
return time.Now().Format(time.RFC3339Nano), nil
default:
return "", errors.New("unsupported auth mode: " + c.authMode)
}
}
// XOAUTH2 auth for net/smtp
type xoauth2Auth struct {
username string
token string
}
func (a *xoauth2Auth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
return "", nil, errors.New("refusing to authenticate over insecure connection")
}
// Microsoft Learn XOAUTH2 Format: user=<user>\x01auth=Bearer <token>\x01\x01 :contentReference[oaicite:4]{index=4}
resp := fmt.Sprintf("user=%s\x01auth=Bearer %s\x01\x01", a.username, a.token)
return "XOAUTH2", []byte(resp), nil
}
func (a *xoauth2Auth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
return nil, errors.New("unexpected server challenge during XOAUTH2 auth")
}
return nil, nil
}
func (c *Client) sendWithXOAUTH2(m *gomail.Message, rcpt string) error {
if c.oauth == nil {
return errors.New("oauth2 provider not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
token, err := c.oauth.getToken(ctx)
if err != nil {
return fmt.Errorf("oauth2 token error: %w", err)
}
// write gomail.Message to RFC822
var buf bytes.Buffer
if _, err := m.WriteTo(&buf); err != nil {
return err
}
msg := buf.Bytes()
addr := fmt.Sprintf("%s:%d", c.host, c.port)
tlsCfg := &tls.Config{
ServerName: c.host,
InsecureSkipVerify: c.insecure,
}
var (
conn net.Conn
cl *smtp.Client
)
switch c.security {
case "ssl":
conn, err = tls.Dial("tcp", addr, tlsCfg)
if err != nil {
return err
}
cl, err = smtp.NewClient(conn, c.host)
if err != nil {
_ = conn.Close()
return err
}
case "starttls", "":
conn, err = net.Dial("tcp", addr)
if err != nil {
return err
}
cl, err = smtp.NewClient(conn, c.host)
if err != nil {
_ = conn.Close()
return err
}
// Upgrade with STARTTLS
if ok, _ := cl.Extension("STARTTLS"); ok {
if err := cl.StartTLS(tlsCfg); err != nil {
_ = cl.Close()
return err
}
} else {
_ = cl.Close()
return errors.New("server does not support STARTTLS")
}
default:
return errors.New("unknown smtp security mode: " + c.security)
}
defer func() { _ = cl.Quit() }()
// AUTH XOAUTH2
if err := cl.Auth(&xoauth2Auth{username: c.username, token: token}); err != nil {
return err
}
// MAIL FROM / RCPT TO / DATA
if err := cl.Mail(extractAddress(c.from)); err != nil {
return err
}
if err := cl.Rcpt(rcpt); err != nil {
return err
}
w, err := cl.Data()
if err != nil {
return err
}
if _, err := w.Write(msg); err != nil {
_ = w.Close()
return err
}
return w.Close()
}
func extractAddress(from string) string {
if i := strings.LastIndex(from, "<"); i >= 0 {
if j := strings.LastIndex(from, ">"); j > i {
return strings.TrimSpace(from[i+1 : j])
}
}
return strings.TrimSpace(from)
}

View File

@@ -1,13 +0,0 @@
package kyc
type KycInfo struct {
Type string `json:"type"` // Chinese / Foreigner
LegalName string `json:"legal_name"`
ResidentId string `json:"rsident_id"`
}
type KycAli struct {
ParamType string `json:"param_type"`
IdentifyNum string `json:"identify_num"`
UserName string `json:"user_name"`
}

View File

@@ -1,18 +0,0 @@
package server
import (
"nixcn-cms/middleware"
"nixcn-cms/service/auth"
"nixcn-cms/service/event"
"nixcn-cms/service/user"
"github.com/gin-gonic/gin"
)
func Router(e *gin.Engine) {
// API Services
api := e.Group("/api/v1", middleware.ApiVersionCheck())
auth.Handler(api.Group("/auth"))
user.Handler(api.Group("/user"))
event.Handler(api.Group("/event"))
}

View File

@@ -1,32 +1,44 @@
package server package server
import ( import (
"context"
"log/slog"
"net"
"net/http" "net/http"
"nixcn-cms/logger" "nixcn-cms/api"
"nixcn-cms/middleware"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
) )
func Start() { func Start(ctx context.Context) {
r := gin.Default() if !viper.GetBool("server.debug_mode") {
r.Use(logger.Gin(), gin.Recovery()) gin.SetMode(gin.ReleaseMode)
gin.DisableConsoleColor()
}
Router(r) r := gin.New()
r.Use(otelgin.Middleware(viper.GetString("server.service_name")))
r.Use(middleware.GinLogger())
r.Use(gin.Recovery())
api.Handler(r.Group("/api/v1"))
// Start http server // Start http server
server := &http.Server{ server := &http.Server{
Addr: viper.GetString("server.address"), Addr: viper.GetString("server.address"),
Handler: r, Handler: r,
BaseContext: func(net.Listener) context.Context { return ctx },
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
} }
log.Info("Starting server on " + viper.GetString("server.address")) slog.InfoContext(ctx, "[Server] Starting server on "+viper.GetString("server.address"))
if err := server.ListenAndServe(); err != nil { if err := server.ListenAndServe(); err != nil {
log.Errorf("Error starting server: %v\n", err) slog.ErrorContext(ctx, "[Server] Error starting server!", "err", err)
} }
} }

124
service/auth/exchange.go Normal file
View File

@@ -0,0 +1,124 @@
package auth
import (
"fmt"
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const ()
func Exchange(c *gin.Context) {
var exchangeReq struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
}
err := c.ShouldBindJSON(&exchangeReq)
if err != nil {
fmt.Println(err)
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return
}
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUnauthorized).
Build(c)
utils.HttpResponse(c, 401, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return
}
userData := new(data.User)
user, err := userData.GetByUserId(c, userId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeGetUserIdFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return
}
code, err := authcode.NewAuthCode(c, exchangeReq.ClientId, user.Email)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeCodeGenFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return
}
url, err := url.Parse(exchangeReq.RedirectUri)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeInvalidRedirectUri).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return
}
query := url.Query()
query.Set("code", code)
url.RawQuery = query.Encode()
exchangeResp := struct {
RedirectUri string `json:"redirect_uri"`
}{url.String()}
errorCode := new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Build(c)
utils.HttpResponse(c, 200, errorCode, exchangeResp)
}

View File

@@ -7,8 +7,9 @@ import (
) )
func Handler(r *gin.RouterGroup) { func Handler(r *gin.RouterGroup) {
r.GET("/redirect", Redirect, middleware.JWTAuth(false)) r.GET("/redirect", Redirect)
r.POST("/magic", Magic) r.POST("/magic", middleware.ApiVersionCheck(), Magic)
r.POST("/token", Token) r.POST("/token", middleware.ApiVersionCheck(), Token)
r.POST("/refresh", Refresh) r.POST("/refresh", middleware.ApiVersionCheck(), Refresh)
r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), Exchange)
} }

View File

@@ -2,6 +2,7 @@ package auth
import ( import (
"net/url" "net/url"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode" "nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/email" "nixcn-cms/pkgs/email"
"nixcn-cms/pkgs/turnstile" "nixcn-cms/pkgs/turnstile"
@@ -23,27 +24,59 @@ func Magic(c *gin.Context) {
// Parse request // Parse request
var req MagicRequest var req MagicRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
utils.HttpResponse(c, 400, "", "invalid request") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
// Cloudflare turnstile // Cloudflare turnstile
ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP()) ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP())
if err != nil || !ok { if err != nil || !ok {
utils.HttpResponse(c, 403, "", "turnstile failed") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicTurnstileFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 403, errorCode)
return return
} }
code, err := authcode.NewAuthCode(req.ClientId, req.Email) code, err := authcode.NewAuthCode(c, req.ClientId, req.Email)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "code gen failed") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicCodeGenFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
externalUrl := viper.GetString("server.external_url") externalUrl := viper.GetString("server.external_url")
url, err := url.Parse(externalUrl) url, err := url.Parse(externalUrl)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "invalid external url") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicInvalidExternalUrl).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
@@ -60,22 +93,37 @@ func Magic(c *gin.Context) {
uriData := struct { uriData := struct {
Uri string `json:"uri"` Uri string `json:"uri"`
}{url.String()} }{url.String()}
utils.HttpResponse(c, 200, "", "magiclink sent", uriData) utils.HttpResponse(c, 200, "", "magiclink sent", uriData)
return return
} else { } else {
// Send email using resend // Send email using resend
emailClient, err := email.NewSMTPClient() emailClient, err := new(email.Client).NewSMTPClient()
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "invalid email config") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicInvalidEmailConfig).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
emailClient.Send( emailClient.Send(
"NixCN CMS <cms@yuri.nix.org.cn>",
req.Email, req.Email,
"NixCN CMS Email Verify", "NixCN CMS Email Verify",
"<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+url.String()+">"+url.String()+"</a>", "<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+url.String()+">"+url.String()+"</a>",
) )
} }
utils.HttpResponse(c, 200, "", "magic link sent") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Build(c)
utils.HttpResponse(c, 200, errorCode)
} }

View File

@@ -3,6 +3,7 @@ package auth
import ( import (
"net/url" "net/url"
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode" "nixcn-cms/pkgs/authcode"
"nixcn-cms/utils" "nixcn-cms/utils"
@@ -14,71 +15,62 @@ import (
func Redirect(c *gin.Context) { func Redirect(c *gin.Context) {
clientId := c.Query("client_id") clientId := c.Query("client_id")
if clientId == "" { if clientId == "" {
utils.HttpResponse(c, 400, "", "invalid request") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
redirectUri := c.Query("redirect_uri") redirectUri := c.Query("redirect_uri")
if redirectUri == "" { if redirectUri == "" {
utils.HttpResponse(c, 400, "", "invalid request") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
state := c.Query("state") state := c.Query("state")
if state == "" { if state == "" {
utils.HttpResponse(c, 400, "", "invalid request") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
code := c.Query("code") code := c.Query("code")
if code == "" {
userIdOrig, ok := c.Get("user_id")
if !ok || userIdOrig == "" {
utils.HttpResponse(c, 401, "", "unauthorized")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
userData := new(data.User)
user, err := userData.GetByUserId(userId)
if err != nil {
utils.HttpResponse(c, 500, "", "failed to get user id")
return
}
code, err := authcode.NewAuthCode(clientId, user.Email)
if err != nil {
utils.HttpResponse(c, 500, "", "code gen failed")
return
}
url, err := url.Parse(redirectUri)
if err != nil {
utils.HttpResponse(c, 400, "", "invalid redirect uri")
return
}
query := url.Query()
query.Set("code", code)
url.RawQuery = query.Encode()
c.Redirect(302, url.String())
}
// Verify email token // Verify email token
authCode, ok := authcode.VerifyAuthCode(code) authCode, ok := authcode.VerifyAuthCode(c, code)
if !ok { if !ok {
utils.HttpResponse(c, 403, "", "invalid or expired token") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectTokenInvalid).
Build(c)
utils.HttpResponse(c, 403, errorCode)
return return
} }
// Verify if user exists // Verify if user exists
userData := new(data.User) userData := new(data.User)
user, err := userData.GetByEmail(authCode.Email) user, err := userData.GetByEmail(c, authCode.Email)
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
@@ -88,38 +80,86 @@ func Redirect(c *gin.Context) {
user.Email = authCode.Email user.Email = authCode.Email
user.Username = user.UserId.String() user.Username = user.UserId.String()
user.PermissionLevel = 10 user.PermissionLevel = 10
if err := user.Create(); err != nil { if err := user.Create(c); err != nil {
utils.HttpResponse(c, 500, "", "internal server error") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
} else { } else {
utils.HttpResponse(c, 500, "", "internal server error") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
} }
clientData := new(data.Client) clientData := new(data.Client)
client, err := clientData.GetClientByClientId(clientId) client, err := clientData.GetClientByClientId(c, clientId)
if err != nil { if err != nil {
utils.HttpResponse(c, 400, "", "client not found") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectClientNotFound).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
err = client.ValidateRedirectURI(redirectUri) err = client.ValidateRedirectURI(redirectUri)
if err != nil { if err != nil {
utils.HttpResponse(c, 400, "", "redirect uri not match") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectUriMismatch).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
newCode, err := authcode.NewAuthCode(clientId, authCode.Email) newCode, err := authcode.NewAuthCode(c, clientId, authCode.Email)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "internal server error") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
url, err := url.Parse(redirectUri) url, err := url.Parse(redirectUri)
if err != nil { if err != nil {
utils.HttpResponse(c, 400, "", "invalid redirect uri") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectInvalidUri).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
query := url.Query() query := url.Query()

View File

@@ -1,6 +1,7 @@
package auth package auth
import ( import (
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authtoken" "nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils" "nixcn-cms/utils"
@@ -14,7 +15,15 @@ func Refresh(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
utils.HttpResponse(c, 400, "", "invalid request") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
@@ -22,15 +31,31 @@ func Refresh(c *gin.Context) {
Application: viper.GetString("server.application"), Application: viper.GetString("server.application"),
} }
accessToken, err := JwtTool.RefreshAccessToken(req.RefreshToken) accessToken, err := JwtTool.RefreshAccessToken(c, req.RefreshToken)
if err != nil { if err != nil {
utils.HttpResponse(c, 401, "", "invalid refresh token") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRefreshInvalidToken).
SetError(err).
Build(c)
utils.HttpResponse(c, 401, errorCode)
return return
} }
refreshToken, err := JwtTool.RenewRefreshToken(req.RefreshToken) refreshToken, err := JwtTool.RenewRefreshToken(c, req.RefreshToken)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "error renew refresh token") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRefreshRenewFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
@@ -39,5 +64,12 @@ func Refresh(c *gin.Context) {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
}{accessToken, refreshToken} }{accessToken, refreshToken}
utils.HttpResponse(c, 200, "", "success", tokenResp) errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Build(c)
utils.HttpResponse(c, 200, errorCode, tokenResp)
} }

View File

@@ -2,6 +2,7 @@ package auth
import ( import (
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode" "nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/authtoken" "nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils" "nixcn-cms/utils"
@@ -19,20 +20,43 @@ func Token(c *gin.Context) {
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
if err != nil { if err != nil {
utils.HttpResponse(c, 400, "", "invalid request") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
authCode, ok := authcode.VerifyAuthCode(req.Code) authCode, ok := authcode.VerifyAuthCode(c, req.Code)
if !ok { if !ok {
utils.HttpResponse(c, 403, "", "invalid or expired token") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthTokenInvalidToken).
Build(c)
utils.HttpResponse(c, 403, errorCode)
return return
} }
userData := new(data.User) userData := new(data.User)
user, err := userData.GetByEmail(authCode.Email) user, err := userData.GetByEmail(c, authCode.Email)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "internal server error") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
@@ -40,9 +64,17 @@ func Token(c *gin.Context) {
JwtTool := authtoken.Token{ JwtTool := authtoken.Token{
Application: viper.GetString("server.application"), Application: viper.GetString("server.application"),
} }
accessToken, refreshToken, err := JwtTool.IssueTokens(authCode.ClientId, user.UserId) accessToken, refreshToken, err := JwtTool.IssueTokens(c, authCode.ClientId, user.UserId)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "error generating tokens") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthTokenGenFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
@@ -51,5 +83,12 @@ func Token(c *gin.Context) {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
}{accessToken, refreshToken} }{accessToken, refreshToken}
utils.HttpResponse(c, 200, "", "success", tokenResp) errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Build(c)
utils.HttpResponse(c, 200, errorCode, tokenResp)
} }

8
service/common.go Normal file
View File

@@ -0,0 +1,8 @@
package service
import "nixcn-cms/internal/exception"
type CommonResult struct {
HttpCode int
Exception *exception.Builder
}

View File

@@ -2,6 +2,7 @@ package event
import ( import (
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils" "nixcn-cms/utils"
"time" "time"
@@ -13,31 +14,69 @@ func Checkin(c *gin.Context) {
data := new(data.Attendance) data := new(data.Attendance)
userIdOrig, ok := c.Get("user_id") userIdOrig, ok := c.Get("user_id")
if !ok { if !ok {
utils.HttpResponse(c, 403, "", "userid error") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build(c)
utils.HttpResponse(c, 403, errorCode)
return return
} }
userId, err := uuid.Parse(userIdOrig.(string)) userId, err := uuid.Parse(userIdOrig.(string))
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
} }
// Get event id from query // Get event id from query
eventIdOrig, ok := c.GetQuery("event_id") eventIdOrig, ok := c.GetQuery("event_id")
if !ok { if !ok {
utils.HttpResponse(c, 400, "", "undefinded event id") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
// Parse event id to uuid // Parse event id to uuid
eventId, err := uuid.Parse(eventIdOrig) eventId, err := uuid.Parse(eventIdOrig)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "error parsing string to uuid") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
data.UserId = userId data.UserId = userId
code, err := data.GenCheckinCode(eventId) code, err := data.GenCheckinCode(c, eventId)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "error generating code") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventCheckinGenCodeFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
@@ -54,9 +93,17 @@ func CheckinSubmit(c *gin.Context) {
c.ShouldBindJSON(&req) c.ShouldBindJSON(&req)
attendanceData := new(data.Attendance) attendanceData := new(data.Attendance)
err := attendanceData.VerifyCheckinCode(req.ChekinCode) err := attendanceData.VerifyCheckinCode(c, req.ChekinCode)
if err != nil { if err != nil {
utils.HttpResponse(c, 400, "", "error verify checkin code") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
@@ -66,34 +113,79 @@ func CheckinSubmit(c *gin.Context) {
func CheckinQuery(c *gin.Context) { func CheckinQuery(c *gin.Context) {
userIdOrig, ok := c.Get("user_id") userIdOrig, ok := c.Get("user_id")
if !ok { if !ok {
utils.HttpResponse(c, 400, "", "userid error") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
userId, err := uuid.Parse(userIdOrig.(string)) userId, err := uuid.Parse(userIdOrig.(string))
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
eventIdOrig, ok := c.GetQuery("event_id") eventIdOrig, ok := c.GetQuery("event_id")
if !ok { if !ok {
utils.HttpResponse(c, 400, "", "could not found event_id") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
eventId, err := uuid.Parse(eventIdOrig) eventId, err := uuid.Parse(eventIdOrig)
if err != nil { if err != nil {
utils.HttpResponse(c, 400, "", "event_id is not valid") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
attendanceData := new(data.Attendance) attendanceData := new(data.Attendance)
attendance, err := attendanceData.GetAttendance(userId, eventId) attendance, err := attendanceData.GetAttendance(c, userId, eventId)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "database error") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} else if attendance == nil { } else if attendance == nil {
utils.HttpResponse(c, 404, "", "event checkin record not found") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventCheckinQueryRecordNotFound).
Build(c)
utils.HttpResponse(c, 404, errorCode)
return return
} else if attendance.CheckinAt.IsZero() { } else if attendance.CheckinAt.IsZero() {
utils.HttpResponse(c, 200, "", "success", gin.H{"checkin_at": nil}) utils.HttpResponse(c, 200, "", "success", gin.H{"checkin_at": nil})
@@ -104,5 +196,12 @@ func CheckinQuery(c *gin.Context) {
CheckinAt time.Time `json:"checkin_at"` CheckinAt time.Time `json:"checkin_at"`
}{attendance.CheckinAt} }{attendance.CheckinAt}
utils.HttpResponse(c, 200, "", "success", checkInAtResp) errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Build(c)
utils.HttpResponse(c, 200, errorCode, checkInAtResp)
} }

View File

@@ -7,9 +7,9 @@ import (
) )
func Handler(r *gin.RouterGroup) { func Handler(r *gin.RouterGroup) {
r.Use(middleware.JWTAuth(true), middleware.Permission(10)) r.Use(middleware.JWTAuth(), middleware.Permission(10))
r.GET("/info", Info) r.GET("/info", Info)
r.GET("/checkin", Checkin) r.GET("/checkin", Checkin)
r.GET("/checkin/query", CheckinQuery) r.GET("/checkin/query", CheckinQuery)
r.POST("/checkin/submit", CheckinSubmit, middleware.Permission(20)) r.POST("/checkin/submit", middleware.Permission(20), CheckinSubmit)
} }

View File

@@ -2,6 +2,7 @@ package event
import ( import (
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils" "nixcn-cms/utils"
"time" "time"
@@ -13,20 +14,43 @@ func Info(c *gin.Context) {
eventData := new(data.Event) eventData := new(data.Event)
eventIdOrig, ok := c.GetQuery("event_id") eventIdOrig, ok := c.GetQuery("event_id")
if !ok { if !ok {
utils.HttpResponse(c, 400, "", "undefinded event id") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return return
} }
// Parse event id // Parse event id
eventId, err := uuid.Parse(eventIdOrig) eventId, err := uuid.Parse(eventIdOrig)
if err != nil { if err != nil {
utils.HttpResponse(c, 500, "", "error parsing string to uuid") errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build(c)
utils.HttpResponse(c, 500, errorCode)
return return
} }
event, err := eventData.GetEventById(eventId) event, err := eventData.GetEventById(c, eventId)
if err != nil { if err != nil {
utils.HttpResponse(c, 404, "", "event id not found") errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventInfoNotFound).
SetError(err).
Build(c)
utils.HttpResponse(c, 404, errorCode)
return return
} }
@@ -36,5 +60,12 @@ func Info(c *gin.Context) {
EndTime time.Time `json:"end_time"` EndTime time.Time `json:"end_time"`
}{event.Name, event.StartTime, event.EndTime} }{event.Name, event.StartTime, event.EndTime}
utils.HttpResponse(c, 200, "", "success", eventInfoResp) errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Build(c)
utils.HttpResponse(c, 200, errorCode, eventInfoResp)
} }

469
service/user.go Normal file
View File

@@ -0,0 +1,469 @@
package service
import (
"context"
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"nixcn-cms/internal/exception"
"strconv"
"unicode/utf8"
"github.com/google/uuid"
)
type UserService interface {
GetUserInfo(*UserInfoPayload) *UserInfoResult
UpdateUserInfo(*UserInfoPayload) *UserInfoResult
ListUsers(*UserListPayload) *UserListResult
GetUserFullTable(*UserTablePayload) *UserTableResult
CreateUser()
}
type UserServiceImpl struct{}
func NewUserService() UserService {
return &UserServiceImpl{}
}
type UserInfoData struct {
UserId uuid.UUID `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
PermissionLevel uint `json:"permission_level"`
AllowPublic bool `json:"allow_public"`
}
type UserInfoPayload struct {
Context context.Context
UserId uuid.UUID
Data *UserInfoData
}
type UserInfoResult struct {
Common CommonResult
Data *UserInfoData
}
// GetUserInfo
func (self *UserServiceImpl) GetUserInfo(payload *UserInfoPayload) (result *UserInfoResult) {
var err error
userData, err := new(data.User).
GetByUserId(
payload.Context,
&payload.UserId,
)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 404,
Exception: exception,
},
Data: nil,
}
return
}
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: &UserInfoData{
UserId: userData.UserId,
Email: userData.Email,
Username: userData.Username,
Nickname: userData.Nickname,
Subtitle: userData.Subtitle,
Avatar: userData.Avatar,
Bio: userData.Bio,
PermissionLevel: userData.PermissionLevel,
AllowPublic: userData.AllowPublic,
},
}
return
}
// UpdateUserInfo
func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *UserInfoResult) {
var err error
userData := new(data.User).
SetNickname(payload.Data.Nickname).
SetSubtitle(payload.Data.Subtitle).
SetAvatar(payload.Data.Avatar).
SetBio(payload.Data.Bio).
SetAllowPublic(payload.Data.AllowPublic)
if payload.Data.Username != "" {
if len(payload.Data.Username) < 5 || len(payload.Data.Username) >= 255 {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetUsername(payload.Data.Username)
}
if payload.Data.Nickname != "" {
if utf8.RuneCountInString(payload.Data.Nickname) > 24 {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetNickname(payload.Data.Nickname)
}
if payload.Data.Subtitle != "" {
if utf8.RuneCountInString(payload.Data.Subtitle) > 32 {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetSubtitle(payload.Data.Subtitle)
}
if payload.Data.Avatar != "" {
_, err := url.ParseRequestURI(payload.Data.Avatar)
if err != nil {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetAvatar(payload.Data.Avatar)
}
if payload.Data.Bio != "" {
if !cryptography.IsBase64Std(payload.Data.Bio) {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.Bio = payload.Data.Bio
}
err = userData.UpdateByUserID(payload.Context, &payload.UserId)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: nil,
}
return
}
type UserListPayload struct {
Context context.Context
Limit *string
Offset *string
}
type UserListResult struct {
Common CommonResult
Data *[]data.UserSearchDoc `json:"user_list"`
}
// ListUsers
func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserListResult) {
var limit string
if payload.Limit == nil || *payload.Limit == "" {
limit = "0"
}
var offset string
if payload.Offset == nil || *payload.Offset == "" {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(payload.Context)
result = &UserListResult{
Common: CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
} else {
offset = *payload.Offset
}
// Parse string to int64
limitNum, err := strconv.ParseInt(limit, 10, 64)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context)
result = &UserListResult{
Common: CommonResult{
HttpCode: 400,
Exception: exception,
},
Data: nil,
}
return
}
offsetNum, err := strconv.ParseInt(offset, 10, 64)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context)
result = &UserListResult{
Common: CommonResult{
HttpCode: 400,
Exception: exception,
},
Data: nil,
}
return
}
// Get user list from search engine
userList, err := new(data.User).
FastListUsers(payload.Context, &limitNum, &offsetNum)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeSpecific).
SetOriginal(exception.UserListMeilisearchFailed).
SetError(err).
Throw(payload.Context)
result = &UserListResult{
Common: CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
}
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserListResult{
Common: CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: userList,
}
return
}
type UserTablePayload struct {
Context context.Context
}
type UserTableResult struct {
Common CommonResult
Data *[]data.User `json:"user_table"`
}
// ListUserFullTable
func (self *UserServiceImpl) GetUserFullTable(payload *UserTablePayload) (result *UserTableResult) {
var err error
userFullTable, err := new(data.User).
GetFullTable(payload.Context)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Throw(payload.Context)
result = &UserTableResult{
Common: CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserTableResult{
Common: CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: userFullTable,
}
return
}
// CreateUser
func (self *UserServiceImpl) CreateUser() {}

View File

@@ -1,39 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Full(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
utils.HttpResponse(c, 404, "", "user not found")
return
}
users, err := userData.GetFullTable()
if err != nil {
utils.HttpResponse(c, 500, "", "database error")
return
}
userFullResp := struct {
UserTable *[]data.User `json:"user_table"`
}{users}
utils.HttpResponse(c, 200, "", "success", userFullResp)
}

View File

@@ -1,16 +0,0 @@
package user
import (
"nixcn-cms/middleware"
"github.com/gin-gonic/gin"
)
func Handler(r *gin.RouterGroup) {
r.Use(middleware.JWTAuth(true), middleware.Permission(5))
r.GET("/info", Info)
r.PATCH("/update", Update)
r.GET("/list", List, middleware.Permission(20))
r.POST("/full", Full, middleware.Permission(40))
r.POST("/create", Create, middleware.Permission(50))
}

View File

@@ -1,43 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Info(c *gin.Context) {
userData := new(data.User)
userIdOrig, ok := c.Get("user_id")
if !ok {
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
// Get user from database
user, err := userData.GetByUserId(userId)
if err != nil {
utils.HttpResponse(c, 404, "", "user not found")
return
}
userInfoResp := struct {
UserId uuid.UUID `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
PermissionLevel uint `json:"permission_level"`
}{user.UserId, user.Email, user.Username, user.Nickname, user.Subtitle, user.Avatar, user.Bio, user.PermissionLevel}
utils.HttpResponse(c, 200, "", "success", userInfoResp)
}

View File

@@ -1,45 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"strconv"
"github.com/gin-gonic/gin"
)
func List(c *gin.Context) {
// Get limit and offset from query
limit, ok := c.GetQuery("limit")
if !ok {
limit = "0"
}
offset, ok := c.GetQuery("offset")
if !ok {
utils.HttpResponse(c, 400, "", "offset not found")
return
}
// Parse string to int64
limitNum, err := strconv.ParseInt(limit, 10, 64)
if err != nil {
utils.HttpResponse(c, 400, "", "parse string to int error")
return
}
offsetNum, err := strconv.ParseInt(offset, 10, 64)
if err != nil {
utils.HttpResponse(c, 400, "", "parse string to int error")
return
}
// Get user list from search engine
list, err := new(data.User).FastListUsers(limitNum, offsetNum)
if err != nil {
utils.HttpResponse(c, 500, "", "failed list users from meilisearch")
}
userListResp := struct {
List *[]data.UserSearchDoc `json:"list"`
}{list}
utils.HttpResponse(c, 200, "", "success", userListResp)
}

View File

@@ -1,62 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Update(c *gin.Context) {
// New user model
userIdOrig, ok := c.Get("user_id")
if !ok {
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
var ReqInfo data.User
c.BindJSON(&ReqInfo)
// Get user info
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
utils.HttpResponse(c, 500, "", "failed to find user")
return
}
if len(ReqInfo.Email) < 5 || len(ReqInfo.Email) >= 255 {
utils.HttpResponse(c, 400, "", "invilad email")
return
}
userData.Email = ReqInfo.Email
if len(ReqInfo.Username) < 5 || len(ReqInfo.Username) >= 255 {
utils.HttpResponse(c, 400, "", "invilad user name")
return
}
userData.Username = ReqInfo.Username
userData.Nickname = ReqInfo.Nickname
userData.Subtitle = ReqInfo.Subtitle
userData.Avatar = ReqInfo.Avatar
if ReqInfo.Bio != "" {
if !cryptography.IsBase64Std(ReqInfo.Bio) {
utils.HttpResponse(c, 400, "", "invalid base64")
}
}
userData.Bio = ReqInfo.Bio
// Update user info
userData.UpdateByUserID(userId)
utils.HttpResponse(c, 200, "", "success")
}

91
tracer/otel_tracer.go Normal file
View File

@@ -0,0 +1,91 @@
package tracer
import (
"context"
"log/slog"
"time"
"github.com/spf13/viper"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation"
sdklog "go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func Init(ctx context.Context) func(context.Context) error {
endpoint := viper.GetString("tracer.otel_controller_endpoint")
if endpoint == "" {
endpoint = "localhost:4317"
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String(viper.GetString("server.service_name")),
),
)
if err != nil {
slog.Error("[OTEL] Failed to create resource", "err", err)
}
traceExporter, _ := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
metricExporter, _ := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint(endpoint),
otlpmetricgrpc.WithInsecure(),
)
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter,
sdkmetric.WithInterval(5*time.Second))),
)
otel.SetMeterProvider(mp)
logExporter, _ := otlploggrpc.New(ctx,
otlploggrpc.WithEndpoint(endpoint),
otlploggrpc.WithInsecure(),
)
lp := sdklog.NewLoggerProvider(
sdklog.WithResource(res),
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
)
global.SetLoggerProvider(lp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return func(shutCtx context.Context) error {
var errs []error
if err := tp.Shutdown(shutCtx); err != nil {
errs = append(errs, err)
}
if err := mp.Shutdown(shutCtx); err != nil {
errs = append(errs, err)
}
if err := lp.Shutdown(shutCtx); err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
return errs[0]
}
return nil
}
}

View File

@@ -1,30 +1,54 @@
package utils package utils
import "github.com/gin-gonic/gin" import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
)
type RespStatus struct { type RespStatus struct {
Code int `json:"code"` Code int `json:"code"`
ErrorId string `json:"error_id"`
Status string `json:"status"` Status string `json:"status"`
ErrorId string `json:"error_id"`
Data any `json:"data"` Data any `json:"data"`
} }
func HttpResponse(c *gin.Context, code int, errorId string, status string, data ...any) { func render(c *gin.Context, code int, errId string, data []any, abort bool) {
var resp = RespStatus{ resp := RespStatus{
Code: code, Code: code,
ErrorId: errorId, Status: http.StatusText(code),
Status: status, ErrorId: errId,
Data: data,
}
c.JSON(code, resp)
} }
func HttpAbort(c *gin.Context, code int, errorId string, status string, data ...any) { switch len(data) {
var resp = RespStatus{ case 0:
Code: code, resp.Data = nil
ErrorId: errorId, case 1:
Status: status, resp.Data = data[0]
Data: data, default:
resp.Data = data
} }
c.AbortWithStatusJSON(code, resp)
jsonBytes, err := json.Marshal(resp)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Header("Content-Type", "application/json; charset=utf-8")
if abort {
c.AbortWithStatus(code)
} else {
c.Status(code)
}
_, _ = c.Writer.Write(jsonBytes)
}
func HttpResponse(c *gin.Context, code int, errId string, data ...any) {
render(c, code, errId, data, false)
}
func HttpAbort(c *gin.Context, code int, errId string, data ...any) {
render(c, code, errId, data, true)
} }