From 4f7632af5310794e36f7bcfa7d3df2c7157c96ab Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 01:41:39 +0800 Subject: [PATCH 001/195] Fix gitea workflow Signed-off-by: Asai Neko --- .gitea/workflow/check.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitea/workflow/check.yaml b/.gitea/workflow/check.yaml index 2b78c23..68dff3d 100644 --- a/.gitea/workflow/check.yaml +++ b/.gitea/workflow/check.yaml @@ -1,6 +1,10 @@ name: Check build frontend and backend run-name: ${{ gitea.actor }} is building nixcn-cms check -on: [push] +on: + push: + branches: + - main + - develop jobs: build-frontend: -- 2.49.1 From d90e22b641913fa917c35b099a8fa7098367ec42 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 01:48:18 +0800 Subject: [PATCH 002/195] Fix gitea workflow name Signed-off-by: Asai Neko --- .gitea/{workflow => workflows}/check.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .gitea/{workflow => workflows}/check.yaml (100%) diff --git a/.gitea/workflow/check.yaml b/.gitea/workflows/check.yaml similarity index 100% rename from .gitea/workflow/check.yaml rename to .gitea/workflows/check.yaml -- 2.49.1 From 4cd4a8cae67df46d6367985dd7853ce71c37315b Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 01:53:36 +0800 Subject: [PATCH 003/195] Fix gitea workflows Signed-off-by: Asai Neko --- .gitea/workflows/check.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/check.yaml b/.gitea/workflows/check.yaml index 68dff3d..ccc137c 100644 --- a/.gitea/workflows/check.yaml +++ b/.gitea/workflows/check.yaml @@ -1,10 +1,6 @@ name: Check build frontend and backend run-name: ${{ gitea.actor }} is building nixcn-cms check -on: - push: - branches: - - main - - develop +on: [push] jobs: build-frontend: @@ -26,7 +22,7 @@ jobs: run: corepack enable - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install - name: Build frontend run: pnpm build -- 2.49.1 From 18fa741e4d188457204b855d3030f776a315e58a Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 02:05:15 +0800 Subject: [PATCH 004/195] Remove gitea actions Signed-off-by: Asai Neko --- .gitea/workflows/check.yaml | 53 ------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 .gitea/workflows/check.yaml diff --git a/.gitea/workflows/check.yaml b/.gitea/workflows/check.yaml deleted file mode 100644 index ccc137c..0000000 --- a/.gitea/workflows/check.yaml +++ /dev/null @@ -1,53 +0,0 @@ -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 ./... -- 2.49.1 From 3f535a824948eb4b481c5858b685e249a47e3606 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 09:23:59 +0800 Subject: [PATCH 005/195] Split containerfile, move to container folder Signed-off-by: Asai Neko --- container/backend.Containerfile | 11 +++++++++++ .../client-cms.Containerfile | 14 +------------- 2 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 container/backend.Containerfile rename Containerfile => container/client-cms.Containerfile (52%) diff --git a/container/backend.Containerfile b/container/backend.Containerfile new file mode 100644 index 0000000..b20ef03 --- /dev/null +++ b/container/backend.Containerfile @@ -0,0 +1,11 @@ +FROM docker.io/golang:1.25.5-alpine AS backend-build +WORKDIR /app +COPY . /app +RUN go mod tidy && \ + go build -o /app/nixcn-cms + +FROM docker.io/alpine:3.23 +WORKDIR /app +COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms +EXPOSE 8000 +ENTRYPOINT [ "/app/nixcn-cms" ] diff --git a/Containerfile b/container/client-cms.Containerfile similarity index 52% rename from Containerfile rename to container/client-cms.Containerfile index 795094f..68ba3fe 100644 --- a/Containerfile +++ b/container/client-cms.Containerfile @@ -7,20 +7,8 @@ ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . RUN just build-client-cms -FROM docker.io/busybox:1.37 AS client-cms +FROM docker.io/busybox:1.37 WORKDIR /app COPY --from=client-build /app/.outputs/client/cms/dist . EXPOSE 3000 ENTRYPOINT ["httpd", "-f", "-p", "3000", "-h", "/app", "-v"] - -FROM docker.io/golang:1.25.5-alpine AS backend-build -WORKDIR /app -COPY . /app -RUN go mod tidy && \ - go build -o /app/nixcn-cms - -FROM docker.io/alpine:3.23 AS backend -WORKDIR /app -COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms -EXPOSE 8000 -ENTRYPOINT [ "/app/nixcn-cms" ] -- 2.49.1 From 9ded703143e58b1a3ca3892310d3dc1f13732385 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 09:45:46 +0800 Subject: [PATCH 006/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 68ba3fe..788117c 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -9,6 +9,6 @@ RUN just build-client-cms FROM docker.io/busybox:1.37 WORKDIR /app -COPY --from=client-build /app/.outputs/client/cms/dist . +COPY --from=client-cms-build /app/.outputs/client/cms/dist . EXPOSE 3000 ENTRYPOINT ["httpd", "-f", "-p", "3000", "-h", "/app", "-v"] -- 2.49.1 From 1c7192db175ab66f87c213fb3d8c5092a2fd36c7 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 09:54:54 +0800 Subject: [PATCH 007/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 788117c..a08b7f4 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -1,5 +1,5 @@ FROM docker.io/node:22-alpine AS client-cms-build -RUN apk add just -y +RUN apk add just RUN npm install -g corepack && \ corepack enable WORKDIR /app -- 2.49.1 From 9fb67ce2be9642f7433dc2e3ba696e935a23781b Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 09:59:56 +0800 Subject: [PATCH 008/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index a08b7f4..d037838 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -5,7 +5,7 @@ RUN npm install -g corepack && \ WORKDIR /app ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . -RUN just build-client-cms +RUN cd client/cms && pnpm run build --outDir .outputs/client/cms/dist FROM docker.io/busybox:1.37 WORKDIR /app -- 2.49.1 From e480bd65483c799a9da261d38c966f5e6fc3c05e Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 10:01:41 +0800 Subject: [PATCH 009/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index d037838..8aacf08 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -5,6 +5,7 @@ RUN npm install -g corepack && \ WORKDIR /app ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . +RUN cd client/cms && pnpm install RUN cd client/cms && pnpm run build --outDir .outputs/client/cms/dist FROM docker.io/busybox:1.37 -- 2.49.1 From 2b5f55f35961c5b211e5f092701af1d21c51f344 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 10:05:21 +0800 Subject: [PATCH 010/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 8aacf08..831e7ab 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -5,6 +5,7 @@ RUN npm install -g corepack && \ WORKDIR /app ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . +RUN mkdir -p .outputs/client/cms RUN cd client/cms && pnpm install RUN cd client/cms && pnpm run build --outDir .outputs/client/cms/dist -- 2.49.1 From a56333fda807ed2803670eb32dcd0d0a563981f1 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 10:07:40 +0800 Subject: [PATCH 011/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 831e7ab..37e5159 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -5,7 +5,7 @@ RUN npm install -g corepack && \ WORKDIR /app ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . -RUN mkdir -p .outputs/client/cms +RUN mkdir -p .outputs/client/cms/dist RUN cd client/cms && pnpm install RUN cd client/cms && pnpm run build --outDir .outputs/client/cms/dist -- 2.49.1 From 140a3070d6e390ed19c4377ce437bb9c4a399394 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 10:42:47 +0800 Subject: [PATCH 012/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 37e5159..56bb6b1 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -7,7 +7,7 @@ ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . RUN mkdir -p .outputs/client/cms/dist RUN cd client/cms && pnpm install -RUN cd client/cms && pnpm run build --outDir .outputs/client/cms/dist +RUN cd client/cms && pnpm run build --outDir /app/.outputs/client/cms/dist FROM docker.io/busybox:1.37 WORKDIR /app -- 2.49.1 From e3c0b60337f564abbbb802e4e34ff51dfd35ce58 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 16:47:27 +0800 Subject: [PATCH 013/195] Fix client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 56bb6b1..76a8860 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -5,7 +5,6 @@ RUN npm install -g corepack && \ WORKDIR /app ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL COPY . . -RUN mkdir -p .outputs/client/cms/dist RUN cd client/cms && pnpm install RUN cd client/cms && pnpm run build --outDir /app/.outputs/client/cms/dist -- 2.49.1 From 89e7f1a41ac8233ddaf72dce1e6851638a66a6ed Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Wed, 28 Jan 2026 18:19:24 +0800 Subject: [PATCH 014/195] WIP: Restructing auth api and service Signed-off-by: Asai Neko --- api/auth/exchange.go | 0 api/auth/magic.go | 0 api/auth/redirect.go | 0 api/auth/refresh.go | 0 api/auth/token.go | 0 api/user/full.go | 4 +- api/user/handler.go | 6 +- api/user/info.go | 4 +- api/user/list.go | 4 +- api/user/update.go | 4 +- service/auth/exchange.go | 124 ----- service/auth/handler.go | 15 - service/auth/redirect.go | 170 ------- service/auth/refresh.go | 75 --- service/auth/token.go | 94 ---- service/event/checkin.go | 207 -------- service/event/create.go | 1 - service/event/handler.go | 15 - service/event/info.go | 71 --- service/event/list.go | 1 - service/event/update.go | 1 - service/service_auth/exchange.go | 129 +++++ service/{auth => service_auth}/magic.go | 147 ++++-- service/service_auth/redirect.go | 27 ++ service/service_auth/service.go | 12 + service/service_user/create_user.go | 3 + service/service_user/get_user_info.go | 94 ++++ service/service_user/list_user_full_table.go | 65 +++ service/service_user/list_users.go | 138 ++++++ service/service_user/service.go | 15 + service/service_user/update_user_info.go | 177 +++++++ service/{ => shared}/common.go | 2 +- service/user.go | 469 ------------------- 33 files changed, 773 insertions(+), 1301 deletions(-) create mode 100644 api/auth/exchange.go create mode 100644 api/auth/magic.go create mode 100644 api/auth/redirect.go create mode 100644 api/auth/refresh.go create mode 100644 api/auth/token.go delete mode 100644 service/auth/exchange.go delete mode 100644 service/auth/handler.go delete mode 100644 service/auth/redirect.go delete mode 100644 service/auth/refresh.go delete mode 100644 service/auth/token.go delete mode 100644 service/event/checkin.go delete mode 100644 service/event/create.go delete mode 100644 service/event/handler.go delete mode 100644 service/event/info.go delete mode 100644 service/event/list.go delete mode 100644 service/event/update.go create mode 100644 service/service_auth/exchange.go rename service/{auth => service_auth}/magic.go (52%) create mode 100644 service/service_auth/redirect.go create mode 100644 service/service_auth/service.go create mode 100644 service/service_user/create_user.go create mode 100644 service/service_user/get_user_info.go create mode 100644 service/service_user/list_user_full_table.go create mode 100644 service/service_user/list_users.go create mode 100644 service/service_user/service.go create mode 100644 service/service_user/update_user_info.go rename service/{ => shared}/common.go (87%) delete mode 100644 service/user.go diff --git a/api/auth/exchange.go b/api/auth/exchange.go new file mode 100644 index 0000000..e69de29 diff --git a/api/auth/magic.go b/api/auth/magic.go new file mode 100644 index 0000000..e69de29 diff --git a/api/auth/redirect.go b/api/auth/redirect.go new file mode 100644 index 0000000..e69de29 diff --git a/api/auth/refresh.go b/api/auth/refresh.go new file mode 100644 index 0000000..e69de29 diff --git a/api/auth/token.go b/api/auth/token.go new file mode 100644 index 0000000..e69de29 diff --git a/api/user/full.go b/api/user/full.go index 5c434af..a89c6cc 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -2,14 +2,14 @@ package user import ( "nixcn-cms/internal/exception" - "nixcn-cms/service" + "nixcn-cms/service/service_user" "nixcn-cms/utils" "github.com/gin-gonic/gin" ) func (self *UserHandler) Full(c *gin.Context) { - userTablePayload := &service.UserTablePayload{ + userTablePayload := &service_user.UserTablePayload{ Context: c, } diff --git a/api/user/handler.go b/api/user/handler.go index 1d2ac0f..0ccd3e5 100644 --- a/api/user/handler.go +++ b/api/user/handler.go @@ -2,17 +2,17 @@ package user import ( "nixcn-cms/middleware" - "nixcn-cms/service" + "nixcn-cms/service/service_user" "github.com/gin-gonic/gin" ) type UserHandler struct { - svc service.UserService + svc service_user.UserService } func ApiHandler(r *gin.RouterGroup) { - userSvc := service.NewUserService() + userSvc := service_user.NewUserService() userHandler := &UserHandler{userSvc} r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(5)) diff --git a/api/user/info.go b/api/user/info.go index 2e506e8..bb8cc0a 100644 --- a/api/user/info.go +++ b/api/user/info.go @@ -2,7 +2,7 @@ package user import ( "nixcn-cms/internal/exception" - "nixcn-cms/service" + "nixcn-cms/service/service_user" "nixcn-cms/utils" "github.com/gin-gonic/gin" @@ -39,7 +39,7 @@ func (self *UserHandler) Info(c *gin.Context) { return } - UserInfoPayload := &service.UserInfoPayload{ + UserInfoPayload := &service_user.UserInfoPayload{ Context: c, UserId: userId, Data: nil, diff --git a/api/user/list.go b/api/user/list.go index 0c4eeee..f4fce52 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -2,7 +2,7 @@ package user import ( "nixcn-cms/internal/exception" - "nixcn-cms/service" + "nixcn-cms/service/service_user" "nixcn-cms/utils" "github.com/gin-gonic/gin" @@ -29,7 +29,7 @@ func (self *UserHandler) List(c *gin.Context) { return } - userListPayload := &service.UserListPayload{ + userListPayload := &service_user.UserListPayload{ Context: c, Limit: query.Limit, Offset: query.Offset, diff --git a/api/user/update.go b/api/user/update.go index 18ab94a..9382774 100644 --- a/api/user/update.go +++ b/api/user/update.go @@ -2,7 +2,7 @@ package user import ( "nixcn-cms/internal/exception" - "nixcn-cms/service" + "nixcn-cms/service/service_user" "nixcn-cms/utils" "github.com/gin-gonic/gin" @@ -38,7 +38,7 @@ func (self *UserHandler) Update(c *gin.Context) { return } - userInfoPayload := &service.UserInfoPayload{ + userInfoPayload := &service_user.UserInfoPayload{ Context: c, UserId: userId, } diff --git a/service/auth/exchange.go b/service/auth/exchange.go deleted file mode 100644 index 89fe02e..0000000 --- a/service/auth/exchange.go +++ /dev/null @@ -1,124 +0,0 @@ -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) -} diff --git a/service/auth/handler.go b/service/auth/handler.go deleted file mode 100644 index a421d6d..0000000 --- a/service/auth/handler.go +++ /dev/null @@ -1,15 +0,0 @@ -package auth - -import ( - "nixcn-cms/middleware" - - "github.com/gin-gonic/gin" -) - -func Handler(r *gin.RouterGroup) { - r.GET("/redirect", Redirect) - r.POST("/magic", middleware.ApiVersionCheck(), Magic) - r.POST("/token", middleware.ApiVersionCheck(), Token) - r.POST("/refresh", middleware.ApiVersionCheck(), Refresh) - r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), Exchange) -} diff --git a/service/auth/redirect.go b/service/auth/redirect.go deleted file mode 100644 index 67a5e33..0000000 --- a/service/auth/redirect.go +++ /dev/null @@ -1,170 +0,0 @@ -package auth - -import ( - "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" - "gorm.io/gorm" -) - -func Redirect(c *gin.Context) { - clientId := c.Query("client_id") - if clientId == "" { - 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 - } - - redirectUri := c.Query("redirect_uri") - if redirectUri == "" { - 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 - } - - state := c.Query("state") - if state == "" { - 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 - } - - code := c.Query("code") - - // Verify email token - authCode, ok := authcode.VerifyAuthCode(c, code) - if !ok { - 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 - } - - // Verify if user exists - userData := new(data.User) - user, err := userData.GetByEmail(c, authCode.Email) - - if err != nil { - if err == gorm.ErrRecordNotFound { - // Create user - user.UUID = uuid.New() - user.UserId = uuid.New() - user.Email = authCode.Email - user.Username = user.UserId.String() - user.PermissionLevel = 10 - if err := user.Create(c); err != nil { - 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 - } - } else { - 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 - } - } - - clientData := new(data.Client) - client, err := clientData.GetClientByClientId(c, clientId) - if err != nil { - 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 - } - - err = client.ValidateRedirectURI(redirectUri) - if err != nil { - 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 - } - - newCode, err := authcode.NewAuthCode(c, clientId, authCode.Email) - if err != nil { - 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 - } - - url, err := url.Parse(redirectUri) - if err != nil { - 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 - } - query := url.Query() - query.Set("code", newCode) - url.RawQuery = query.Encode() - - c.Redirect(302, url.String()) -} diff --git a/service/auth/refresh.go b/service/auth/refresh.go deleted file mode 100644 index 248fd05..0000000 --- a/service/auth/refresh.go +++ /dev/null @@ -1,75 +0,0 @@ -package auth - -import ( - "nixcn-cms/internal/exception" - "nixcn-cms/pkgs/authtoken" - "nixcn-cms/utils" - - "github.com/gin-gonic/gin" - "github.com/spf13/viper" -) - -func Refresh(c *gin.Context) { - var req struct { - RefreshToken string `json:"refresh_token"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - 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 - } - - JwtTool := authtoken.Token{ - Application: viper.GetString("server.application"), - } - - accessToken, err := JwtTool.RefreshAccessToken(c, req.RefreshToken) - if err != nil { - 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 - } - - refreshToken, err := JwtTool.RenewRefreshToken(c, req.RefreshToken) - if err != nil { - 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 - } - - tokenResp := struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - }{accessToken, refreshToken} - - 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) -} diff --git a/service/auth/token.go b/service/auth/token.go deleted file mode 100644 index a050496..0000000 --- a/service/auth/token.go +++ /dev/null @@ -1,94 +0,0 @@ -package auth - -import ( - "nixcn-cms/data" - "nixcn-cms/internal/exception" - "nixcn-cms/pkgs/authcode" - "nixcn-cms/pkgs/authtoken" - "nixcn-cms/utils" - - "github.com/gin-gonic/gin" - "github.com/spf13/viper" -) - -type TokenRequest struct { - Code string `json:"code"` -} - -func Token(c *gin.Context) { - var req TokenRequest - - err := c.ShouldBindJSON(&req) - if err != nil { - 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 - } - - authCode, ok := authcode.VerifyAuthCode(c, req.Code) - if !ok { - 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 - } - - userData := new(data.User) - user, err := userData.GetByEmail(c, authCode.Email) - if err != nil { - 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 - } - - // Generate jwt - JwtTool := authtoken.Token{ - Application: viper.GetString("server.application"), - } - accessToken, refreshToken, err := JwtTool.IssueTokens(c, authCode.ClientId, user.UserId) - if err != nil { - 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 - } - - tokenResp := struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - }{accessToken, refreshToken} - - 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) -} diff --git a/service/event/checkin.go b/service/event/checkin.go deleted file mode 100644 index 5205c62..0000000 --- a/service/event/checkin.go +++ /dev/null @@ -1,207 +0,0 @@ -package event - -import ( - "nixcn-cms/data" - "nixcn-cms/internal/exception" - "nixcn-cms/utils" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -func Checkin(c *gin.Context) { - data := new(data.Attendance) - userIdOrig, ok := c.Get("user_id") - if !ok { - 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 - } - userId, err := uuid.Parse(userIdOrig.(string)) - if err != nil { - 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 - eventIdOrig, ok := c.GetQuery("event_id") - if !ok { - 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 - } - - // Parse event id to uuid - eventId, err := uuid.Parse(eventIdOrig) - if err != nil { - 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 - } - data.UserId = userId - code, err := data.GenCheckinCode(c, eventId) - if err != nil { - 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 - } - - checkinCodeResp := struct { - CheckinCode *string `json:"checkin_code"` - }{code} - utils.HttpResponse(c, 200, "", "success", checkinCodeResp) -} - -func CheckinSubmit(c *gin.Context) { - var req struct { - ChekinCode string `json:"checkin_code"` - } - c.ShouldBindJSON(&req) - - attendanceData := new(data.Attendance) - err := attendanceData.VerifyCheckinCode(c, req.ChekinCode) - if err != nil { - 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 - } - - utils.HttpResponse(c, 200, "", "success") -} - -func CheckinQuery(c *gin.Context) { - userIdOrig, ok := c.Get("user_id") - if !ok { - 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 - } - userId, err := uuid.Parse(userIdOrig.(string)) - if err != nil { - 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 - } - - eventIdOrig, ok := c.GetQuery("event_id") - if !ok { - 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 - } - eventId, err := uuid.Parse(eventIdOrig) - if err != nil { - 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 - } - - attendanceData := new(data.Attendance) - attendance, err := attendanceData.GetAttendance(c, userId, eventId) - - if err != nil { - 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 - } else if attendance == nil { - 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 - } else if attendance.CheckinAt.IsZero() { - utils.HttpResponse(c, 200, "", "success", gin.H{"checkin_at": nil}) - return - } - - checkInAtResp := struct { - CheckinAt time.Time `json:"checkin_at"` - }{attendance.CheckinAt} - - 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) -} diff --git a/service/event/create.go b/service/event/create.go deleted file mode 100644 index 0e4b82e..0000000 --- a/service/event/create.go +++ /dev/null @@ -1 +0,0 @@ -package event diff --git a/service/event/handler.go b/service/event/handler.go deleted file mode 100644 index 875c9f3..0000000 --- a/service/event/handler.go +++ /dev/null @@ -1,15 +0,0 @@ -package event - -import ( - "nixcn-cms/middleware" - - "github.com/gin-gonic/gin" -) - -func Handler(r *gin.RouterGroup) { - r.Use(middleware.JWTAuth(), middleware.Permission(10)) - r.GET("/info", Info) - r.GET("/checkin", Checkin) - r.GET("/checkin/query", CheckinQuery) - r.POST("/checkin/submit", middleware.Permission(20), CheckinSubmit) -} diff --git a/service/event/info.go b/service/event/info.go deleted file mode 100644 index 351de99..0000000 --- a/service/event/info.go +++ /dev/null @@ -1,71 +0,0 @@ -package event - -import ( - "nixcn-cms/data" - "nixcn-cms/internal/exception" - "nixcn-cms/utils" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -func Info(c *gin.Context) { - eventData := new(data.Event) - eventIdOrig, ok := c.GetQuery("event_id") - if !ok { - 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 - } - - // Parse event id - eventId, err := uuid.Parse(eventIdOrig) - if err != nil { - 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 - } - - event, err := eventData.GetEventById(c, eventId) - if err != nil { - 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 - } - - eventInfoResp := struct { - Name string `json:"name"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - }{event.Name, event.StartTime, event.EndTime} - - 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) -} diff --git a/service/event/list.go b/service/event/list.go deleted file mode 100644 index 0e4b82e..0000000 --- a/service/event/list.go +++ /dev/null @@ -1 +0,0 @@ -package event diff --git a/service/event/update.go b/service/event/update.go deleted file mode 100644 index 0e4b82e..0000000 --- a/service/event/update.go +++ /dev/null @@ -1 +0,0 @@ -package event diff --git a/service/service_auth/exchange.go b/service/service_auth/exchange.go new file mode 100644 index 0000000..b4396ee --- /dev/null +++ b/service/service_auth/exchange.go @@ -0,0 +1,129 @@ +package service_auth + +import ( + "context" + "net/url" + "nixcn-cms/data" + "nixcn-cms/internal/authcode" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + + "github.com/google/uuid" +) + +type ExchangeData struct { + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` + State string `json:"state"` +} + +type ExchangePayload struct { + Context context.Context + UserId uuid.UUID + Data *ExchangeData +} + +type ExchangeResult struct { + Common shared.CommonResult + Data *struct { + RedirectUri string `json:"redirect_uri"` + } +} + +func (self *AuthServiceImpl) Exchange(payload *ExchangePayload) (result *ExchangeResult) { + var err error + + userData, err := new(data.User). + GetByUserId(payload.Context, &payload.UserId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthExchangeGetUserIdFailed). + SetError(err). + Throw(payload.Context) + + result = &ExchangeResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, userData.Email) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthExchangeCodeGenFailed). + SetError(err). + Throw(payload.Context) + + result = &ExchangeResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + url, err := url.Parse(payload.Data.RedirectUri) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthExchangeInvalidRedirectUri). + SetError(err). + Throw(payload.Context) + + result = &ExchangeResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + query := url.Query() + query.Set("code", code) + url.RawQuery = query.Encode() + + exception := new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + resultData := struct { + RedirectUri string `json:"redirect_uri"` + }{url.String()} + + result = &ExchangeResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &resultData, + } + + return +} diff --git a/service/auth/magic.go b/service/service_auth/magic.go similarity index 52% rename from service/auth/magic.go rename to service/service_auth/magic.go index a5a7568..15a6e2c 100644 --- a/service/auth/magic.go +++ b/service/service_auth/magic.go @@ -1,91 +1,112 @@ -package auth +package service_auth import ( + "context" "net/url" + "nixcn-cms/internal/authcode" + "nixcn-cms/internal/email" "nixcn-cms/internal/exception" - "nixcn-cms/pkgs/authcode" - "nixcn-cms/pkgs/email" - "nixcn-cms/pkgs/turnstile" - "nixcn-cms/utils" + "nixcn-cms/internal/turnstile" + "nixcn-cms/service/shared" - "github.com/gin-gonic/gin" "github.com/spf13/viper" ) -type MagicRequest struct { +type MagicData struct { ClientId string `json:"client_id"` RedirectUri string `json:"redirect_uri"` State string `json:"state"` Email string `json:"email"` TurnstileToken string `json:"turnstile_token"` + ClientIP string `json:"client_ip"` } -func Magic(c *gin.Context) { - // Parse request - var req MagicRequest - if err := c.ShouldBindJSON(&req); err != nil { - 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 - } +type MagicPayload struct { + Context context.Context + Data *MagicData +} - // Cloudflare turnstile - ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP()) +type MagicResult struct { + Common shared.CommonResult + Data any +} + +func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult) { + var err error + + ok, err := turnstile.VerifyTurnstile(payload.Data.TurnstileToken, payload.Data.ClientIP) if err != nil || !ok { - errorCode := new(exception.Builder). + exception := 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) + Throw(payload.Context) + + result = &MagicResult{ + Common: shared.CommonResult{ + HttpCode: 403, + Exception: exception, + }, + Data: nil, + } + return } - code, err := authcode.NewAuthCode(c, req.ClientId, req.Email) + code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, payload.Data.Email) if err != nil { - errorCode := new(exception.Builder). + exception := 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) + Throw(payload.Context) + + result = &MagicResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + return } externalUrl := viper.GetString("server.external_url") url, err := url.Parse(externalUrl) if err != nil { - errorCode := new(exception.Builder). + exception := 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) + Throw(payload.Context) + + result = &MagicResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + return } url.Path = "/api/v1/auth/redirect" query := url.Query() query.Set("code", code) - query.Set("redirect_uri", req.RedirectUri) - query.Set("state", req.State) - query.Set("client_id", req.ClientId) + query.Set("redirect_uri", payload.Data.RedirectUri) + query.Set("state", payload.Data.State) + query.Set("client_id", payload.Data.ClientId) url.RawQuery = query.Encode() debugMode := viper.GetBool("server.debug_mode") @@ -93,37 +114,71 @@ func Magic(c *gin.Context) { uriData := struct { Uri string `json:"uri"` }{url.String()} - utils.HttpResponse(c, 200, "", "magiclink sent", uriData) + + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceMagic). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &MagicResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: uriData, + } + return } else { - // Send email using resend emailClient, err := new(email.Client).NewSMTPClient() if err != nil { - errorCode := new(exception.Builder). + exception := 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) + Throw(payload.Context) + + result = &MagicResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + return } emailClient.Send( "NixCN CMS ", - req.Email, + payload.Data.Email, "NixCN CMS Email Verify", "

Click the link below to verify your email. This link will expire in 10 minutes.

"+url.String()+"", ) } - errorCode := new(exception.Builder). + exception := 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) + SetError(nil). + Throw(payload.Context) + + result = &MagicResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: nil, + } + + return } diff --git a/service/service_auth/redirect.go b/service/service_auth/redirect.go new file mode 100644 index 0000000..4b612fb --- /dev/null +++ b/service/service_auth/redirect.go @@ -0,0 +1,27 @@ +package service_auth + +import ( + "context" + "nixcn-cms/service/shared" +) + +type RedirectData struct { + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` + State string `json:"state"` + Code string `json:"code"` +} + +type RedirectPayload struct { + Context context.Context + Data *RedirectData +} + +type RedirectResult struct { + Common shared.CommonResult + Data string +} + +func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *RedirectResult) { + +} diff --git a/service/service_auth/service.go b/service/service_auth/service.go new file mode 100644 index 0000000..423aa86 --- /dev/null +++ b/service/service_auth/service.go @@ -0,0 +1,12 @@ +package service_auth + +type AuthService interface { + Exchange(*ExchangePayload) *ExchangeResult + Magic(*MagicPayload) *MagicResult +} + +type AuthServiceImpl struct{} + +func NewAuthService() AuthService { + return &AuthServiceImpl{} +} diff --git a/service/service_user/create_user.go b/service/service_user/create_user.go new file mode 100644 index 0000000..3da2552 --- /dev/null +++ b/service/service_user/create_user.go @@ -0,0 +1,3 @@ +package service_user + +func (self *UserServiceImpl) CreateUser() {} diff --git a/service/service_user/get_user_info.go b/service/service_user/get_user_info.go new file mode 100644 index 0000000..c60e3d4 --- /dev/null +++ b/service/service_user/get_user_info.go @@ -0,0 +1,94 @@ +package service_user + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + + "github.com/google/uuid" +) + +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 shared.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: shared.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: shared.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 +} diff --git a/service/service_user/list_user_full_table.go b/service/service_user/list_user_full_table.go new file mode 100644 index 0000000..f27bd65 --- /dev/null +++ b/service/service_user/list_user_full_table.go @@ -0,0 +1,65 @@ +package service_user + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" +) + +type UserTablePayload struct { + Context context.Context +} + +type UserTableResult struct { + Common shared.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: shared.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: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: userFullTable, + } + + return +} diff --git a/service/service_user/list_users.go b/service/service_user/list_users.go new file mode 100644 index 0000000..4001cf4 --- /dev/null +++ b/service/service_user/list_users.go @@ -0,0 +1,138 @@ +package service_user + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + "strconv" +) + +type UserListPayload struct { + Context context.Context + Limit *string + Offset *string +} + +type UserListResult struct { + Common shared.CommonResult + Data *[]data.UserSearchDoc `json:"user_list"` +} + +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: shared.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: shared.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: shared.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: shared.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: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: userList, + } + + return + +} diff --git a/service/service_user/service.go b/service/service_user/service.go new file mode 100644 index 0000000..5b39ed3 --- /dev/null +++ b/service/service_user/service.go @@ -0,0 +1,15 @@ +package service_user + +type UserService interface { + GetUserInfo(*UserInfoPayload) *UserInfoResult + UpdateUserInfo(*UserInfoPayload) *UserInfoResult + ListUsers(*UserListPayload) *UserListResult + GetUserFullTable(*UserTablePayload) *UserTableResult + CreateUser() +} + +type UserServiceImpl struct{} + +func NewUserService() UserService { + return &UserServiceImpl{} +} diff --git a/service/service_user/update_user_info.go b/service/service_user/update_user_info.go new file mode 100644 index 0000000..25b7bc0 --- /dev/null +++ b/service/service_user/update_user_info.go @@ -0,0 +1,177 @@ +package service_user + +import ( + "net/url" + "nixcn-cms/data" + "nixcn-cms/internal/cryptography" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + "unicode/utf8" +) + +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: shared.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: shared.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: shared.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: shared.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: shared.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: shared.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: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: nil, + } + + return +} diff --git a/service/common.go b/service/shared/common.go similarity index 87% rename from service/common.go rename to service/shared/common.go index cb41d95..2dc0c5c 100644 --- a/service/common.go +++ b/service/shared/common.go @@ -1,4 +1,4 @@ -package service +package shared import "nixcn-cms/internal/exception" diff --git a/service/user.go b/service/user.go deleted file mode 100644 index 626881c..0000000 --- a/service/user.go +++ /dev/null @@ -1,469 +0,0 @@ -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() {} -- 2.49.1 From 79dfa8499c0e5699aab673140a75d3e498956c4e Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 00:45:58 +0800 Subject: [PATCH 015/195] Full Restruct API and Services Signed-off-by: Asai Neko --- api/auth/exchange.go | 87 +++ api/auth/handler.go | 17 +- api/auth/magic.go | 54 ++ api/auth/redirect.go | 59 ++ api/auth/refresh.go | 52 ++ api/auth/token.go | 52 ++ api/event/checkin.go | 113 ++++ api/event/handler.go | 16 +- api/event/info.go | 55 ++ api/user/full.go | 10 + api/user/info.go | 12 + api/user/list.go | 12 + api/user/update.go | 14 + docs/docs.go | 1008 ++++++++++++++++++++++++++++++ docs/swagger.json | 979 +++++++++++++++++++++++++++++ docs/swagger.yaml | 646 +++++++++++++++++++ go.mod | 38 +- go.sum | 85 +++ internal/ali_cnrid/kyc.go | 21 +- server/server.go | 16 + service/service_auth/redirect.go | 183 ++++++ service/service_auth/refresh.go | 99 +++ service/service_auth/service.go | 3 + service/service_auth/token.go | 118 ++++ service/service_event/checkin.go | 191 ++++++ service/service_event/info.go | 78 +++ service/service_event/service.go | 14 + 27 files changed, 4011 insertions(+), 21 deletions(-) create mode 100644 api/event/checkin.go create mode 100644 api/event/info.go create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 service/service_auth/refresh.go create mode 100644 service/service_auth/token.go create mode 100644 service/service_event/checkin.go create mode 100644 service/service_event/info.go create mode 100644 service/service_event/service.go diff --git a/api/auth/exchange.go b/api/auth/exchange.go index e69de29..73cc4fe 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -0,0 +1,87 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Exchange handles the authorization code swap process. +// @Summary Exchange Auth Code +// @Description Exchanges client credentials and user session for a specific redirect authorization code. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials" +// @Success 200 {object} service_auth.ExchangeResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal Server Error" +// @Security ApiKeyAuth +// @Router /auth/exchange [post] +func (self *AuthHandler) Exchange(c *gin.Context) { + var exchangeData service_auth.ExchangeData + + if err := c.ShouldBindJSON(&exchangeData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + 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). + SetError(nil). + Throw(c). + String() + + 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). + Throw(c). + String() + + utils.HttpResponse(c, 500, errorCode) + return + } + + result := self.svc.Exchange(&service_auth.ExchangePayload{ + Context: c, + UserId: userId, + Data: &exchangeData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/auth/handler.go b/api/auth/handler.go index ad630b1..a920165 100644 --- a/api/auth/handler.go +++ b/api/auth/handler.go @@ -1,8 +1,23 @@ package auth import ( + "nixcn-cms/middleware" + "nixcn-cms/service/service_auth" + "github.com/gin-gonic/gin" ) -func ApiHandler(r *gin.RouterGroup) { +type AuthHandler struct { + svc service_auth.AuthService +} + +func ApiHandler(r *gin.RouterGroup) { + authSvc := service_auth.NewAuthService() + authHandler := &AuthHandler{authSvc} + + r.GET("/redirect", authHandler.Redirect) + r.POST("/magic", middleware.ApiVersionCheck(), authHandler.Magic) + r.POST("/token", middleware.ApiVersionCheck(), authHandler.Token) + r.POST("/refresh", middleware.ApiVersionCheck(), authHandler.Refresh) + r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), authHandler.Exchange) } diff --git a/api/auth/magic.go b/api/auth/magic.go index e69de29..11c3528 100644 --- a/api/auth/magic.go +++ b/api/auth/magic.go @@ -0,0 +1,54 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Magic handles the "Magic Link" authentication request. +// @Summary Request Magic Link +// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.MagicData true "Magic Link Request Data" +// @Success 200 {object} service_auth.MagicResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 403 {string} string "Turnstile Verification Failed" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/magic [post] +func (self *AuthHandler) Magic(c *gin.Context) { + var magicData service_auth.MagicData + + if err := c.ShouldBindJSON(&magicData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceMagic). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + magicData.ClientIP = c.ClientIP() + + result := self.svc.Magic(&service_auth.MagicPayload{ + Context: c, + Data: &magicData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/auth/redirect.go b/api/auth/redirect.go index e69de29..8e2742b 100644 --- a/api/auth/redirect.go +++ b/api/auth/redirect.go @@ -0,0 +1,59 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Redirect handles the post-verification callback and redirects the user to the target application. +// @Summary Handle Auth Callback and Redirect +// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. +// @Tags Authentication +// @Accept x-www-form-urlencoded +// @Produce html +// @Param client_id query string true "Client Identifier" +// @Param redirect_uri query string true "Target Redirect URI" +// @Param code query string true "Temporary Verification Code" +// @Param state query string false "Opaque state used to maintain state between the request and callback" +// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code" +// @Failure 400 {string} string "Invalid Input / Client Not Found / URI Mismatch" +// @Failure 403 {string} string "Invalid or Expired Verification Code" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/redirect [get] +func (self *AuthHandler) Redirect(c *gin.Context) { + data := &service_auth.RedirectData{ + ClientId: c.Query("client_id"), + RedirectUri: c.Query("redirect_uri"), + State: c.Query("state"), + Code: c.Query("code"), + } + + if data.ClientId == "" || data.RedirectUri == "" || data.Code == "" { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(nil). + Throw(c). + String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Redirect(&service_auth.RedirectPayload{ + Context: c, + Data: data, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + c.Redirect(302, result.Data) +} diff --git a/api/auth/refresh.go b/api/auth/refresh.go index e69de29..d6ee131 100644 --- a/api/auth/refresh.go +++ b/api/auth/refresh.go @@ -0,0 +1,52 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Refresh handles the token rotation process. +// @Summary Refresh Access Token +// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.RefreshData true "Refresh Token Body" +// @Success 200 {object} service_auth.RefreshResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 401 {string} string "Invalid Refresh Token" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/refresh [post] +func (self *AuthHandler) Refresh(c *gin.Context) { + var refreshData service_auth.RefreshData + + if err := c.ShouldBindJSON(&refreshData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Refresh(&service_auth.RefreshPayload{ + Context: c, + Data: &refreshData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/auth/token.go b/api/auth/token.go index e69de29..942f322 100644 --- a/api/auth/token.go +++ b/api/auth/token.go @@ -0,0 +1,52 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Token exchanges an authorization code for access and refresh tokens. +// @Summary Exchange Code for Token +// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.TokenData true "Token Request Body" +// @Success 200 {object} service_auth.TokenResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 403 {string} string "Invalid or Expired Code" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/token [post] +func (self *AuthHandler) Token(c *gin.Context) { + var tokenData service_auth.TokenData + + if err := c.ShouldBindJSON(&tokenData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Token(&service_auth.TokenPayload{ + Context: c, + Data: &tokenData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/event/checkin.go b/api/event/checkin.go new file mode 100644 index 0000000..f980662 --- /dev/null +++ b/api/event/checkin.go @@ -0,0 +1,113 @@ +package event + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_event" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Checkin generates a check-in code for a specific event. +// @Summary Generate Check-in Code +// @Description Creates a temporary check-in code for the authenticated user and event. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} service_event.CheckinResult +// @Router /event/checkin [get] +func (self *EventHandler) Checkin(c *gin.Context) { + userIdOrig, _ := c.Get("user_id") + userId, _ := uuid.Parse(userIdOrig.(string)) + + eventIdOrig := c.Query("event_id") + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c).String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Checkin(&service_event.CheckinPayload{ + Context: c, + UserId: userId, + Data: &service_event.CheckinData{EventId: eventId}, + }) + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data) +} + +// CheckinSubmit validates a check-in code to complete attendance. +// @Summary Submit Check-in Code +// @Description Submits the generated code to mark the user as attended. +// @Tags Event +// @Accept json +// @Produce json +// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data" +// @Success 200 {object} service_event.CheckinSubmitResult +// @Router /event/checkin/submit [post] +func (self *EventHandler) CheckinSubmit(c *gin.Context) { + var data service_event.CheckinSubmitData + if err := c.ShouldBindJSON(&data); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinSubmit). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c).String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.CheckinSubmit(&service_event.CheckinSubmitPayload{ + Context: c, + Data: &data, + }) + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) +} + +// CheckinQuery retrieves the check-in status of a user for an event. +// @Summary Query Check-in Status +// @Description Returns the timestamp of when the user checked in, or null if not yet checked in. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} service_event.CheckinQueryResult +// @Router /event/checkin/query [get] +func (self *EventHandler) CheckinQuery(c *gin.Context) { + userIdOrig, _ := c.Get("user_id") + userId, _ := uuid.Parse(userIdOrig.(string)) + + eventIdOrig := c.Query("event_id") + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c).String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.CheckinQuery(&service_event.CheckinQueryPayload{ + Context: c, + UserId: userId, + Data: &service_event.CheckinQueryData{EventId: eventId}, + }) + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data) +} diff --git a/api/event/handler.go b/api/event/handler.go index 8e182c5..0746218 100644 --- a/api/event/handler.go +++ b/api/event/handler.go @@ -2,10 +2,22 @@ package event import ( "nixcn-cms/middleware" + "nixcn-cms/service/service_event" "github.com/gin-gonic/gin" ) -func ApiHandler(r *gin.RouterGroup) { - r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10)) +type EventHandler struct { + svc service_event.EventService +} + +func ApiHandler(r *gin.RouterGroup) { + eventSvc := service_event.NewEventService() + eventHandler := &EventHandler{eventSvc} + + r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10)) + r.GET("/info", eventHandler.Info) + r.GET("/checkin", eventHandler.Checkin) + r.GET("/checkin/query", eventHandler.CheckinQuery) + r.POST("/checkin/submit", middleware.Permission(20), eventHandler.CheckinSubmit) } diff --git a/api/event/info.go b/api/event/info.go new file mode 100644 index 0000000..0695d3a --- /dev/null +++ b/api/event/info.go @@ -0,0 +1,55 @@ +package event + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_event" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Info retrieves basic information about a specific event. +// @Summary Get Event Information +// @Description Fetches the name, start time, and end time of an event using its UUID. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} service_event.InfoResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 404 {string} string "Event Not Found" +// @Failure 500 {string} string "Internal Server Error" +// @Router /event/info [get] +func (self *EventHandler) Info(c *gin.Context) { + eventIdOrig := c.Query("event_id") + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceInfo). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 500, errorCode) + return + } + + result := self.svc.Info(&service_event.InfoPayload{ + Context: c, + Data: &service_event.InfoData{ + EventId: eventId, + }, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/user/full.go b/api/user/full.go index a89c6cc..c5a2440 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -8,6 +8,16 @@ import ( "github.com/gin-gonic/gin" ) +// Full retrieves the complete list of users directly from the database table. +// @Summary Get Full User Table +// @Description Fetches all user records without pagination. This is typically used for administrative overview or data export. +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} service_user.UserTableResult +// @Failure 500 {string} string "Internal Server Error (Database Error)" +// @Security ApiKeyAuth +// @Router /user/full [get] func (self *UserHandler) Full(c *gin.Context) { userTablePayload := &service_user.UserTablePayload{ Context: c, diff --git a/api/user/info.go b/api/user/info.go index bb8cc0a..bcf21af 100644 --- a/api/user/info.go +++ b/api/user/info.go @@ -9,6 +9,18 @@ import ( "github.com/google/uuid" ) +// Info retrieves the profile information of the currently authenticated user. +// @Summary Get My User Information +// @Description Fetches the complete profile data for the user associated with the provided session/token. +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} service_user.UserInfoResult +// @Failure 403 {string} string "Missing User ID / Unauthorized" +// @Failure 404 {string} string "User Not Found" +// @Failure 500 {string} string "Internal Server Error (UUID Parse Failed)" +// @Security ApiKeyAuth +// @Router /user/info [get] func (self *UserHandler) Info(c *gin.Context) { userIdOrig, ok := c.Get("user_id") if !ok { diff --git a/api/user/list.go b/api/user/list.go index f4fce52..48c9934 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -8,6 +8,18 @@ import ( "github.com/gin-gonic/gin" ) +// List retrieves a paginated list of users from the search engine. +// @Summary List Users +// @Description Fetches a list of users with support for pagination via limit and offset. +// @Tags User +// @Accept json +// @Produce json +// @Param limit query string false "Maximum number of users to return (default 0)" +// @Param offset query string true "Number of users to skip" +// @Success 200 {object} service_user.UserListResult +// @Failure 400 {string} string "Invalid Input (Format Error)" +// @Failure 500 {string} string "Internal Server Error (Search Engine or Missing Offset)" +// @Router /user/list [get] func (self *UserHandler) List(c *gin.Context) { type ListQuery struct { Limit *string `form:"limit"` diff --git a/api/user/update.go b/api/user/update.go index 9382774..ec34c1a 100644 --- a/api/user/update.go +++ b/api/user/update.go @@ -9,6 +9,20 @@ import ( "github.com/google/uuid" ) +// Update modifies the profile information for the currently authenticated user. +// @Summary Update User Information +// @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). +// @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). +// @Tags User +// @Accept json +// @Produce json +// @Param payload body service_user.UserInfoData true "Updated User Profile Data" +// @Success 200 {object} service_user.UserInfoResult +// @Failure 400 {string} string "Invalid Input (Validation Failed)" +// @Failure 403 {string} string "Missing User ID / Unauthorized" +// @Failure 500 {string} string "Internal Server Error (Database Error / UUID Parse Failed)" +// @Security ApiKeyAuth +// @Router /user/update [patch] func (self *UserHandler) Update(c *gin.Context) { userIdOrig, ok := c.Get("user_id") if !ok { diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..c235495 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1008 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/auth/exchange": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Exchanges client credentials and user session for a specific redirect authorization code.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Auth Code", + "parameters": [ + { + "description": "Exchange Request Credentials", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.ExchangeData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.ExchangeResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/magic": { + "post": { + "description": "Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Request Magic Link", + "parameters": [ + { + "description": "Magic Link Request Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.MagicData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.MagicResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Turnstile Verification Failed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/redirect": { + "get": { + "description": "Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "text/html" + ], + "tags": [ + "Authentication" + ], + "summary": "Handle Auth Callback and Redirect", + "parameters": [ + { + "type": "string", + "description": "Client Identifier", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Target Redirect URI", + "name": "redirect_uri", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Temporary Verification Code", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Opaque state used to maintain state between the request and callback", + "name": "state", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Redirect to the provided RedirectUri with a new code", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid Input / Client Not Found / URI Mismatch", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Verification Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Accepts a valid refresh token to issue a new access token and a rotated refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh Access Token", + "parameters": [ + { + "description": "Refresh Token Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.RefreshData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.RefreshResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Invalid Refresh Token", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/token": { + "post": { + "description": "Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Code for Token", + "parameters": [ + { + "description": "Token Request Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.TokenData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.TokenResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/event/checkin": { + "get": { + "description": "Creates a temporary check-in code for the authenticated user and event.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Generate Check-in Code", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinResult" + } + } + } + } + }, + "/event/checkin/query": { + "get": { + "description": "Returns the timestamp of when the user checked in, or null if not yet checked in.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Query Check-in Status", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinQueryResult" + } + } + } + } + }, + "/event/checkin/submit": { + "post": { + "description": "Submits the generated code to mark the user as attended.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Submit Check-in Code", + "parameters": [ + { + "description": "Checkin Code Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitResult" + } + } + } + } + }, + "/event/info": { + "get": { + "description": "Fetches the name, start time, and end time of an event using its UUID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Get Event Information", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.InfoResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Event Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/full": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches all user records without pagination. This is typically used for administrative overview or data export.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get Full User Table", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserTableResult" + } + }, + "500": { + "description": "Internal Server Error (Database Error)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the complete profile data for the user associated with the provided session/token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get My User Information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "User Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/list": { + "get": { + "description": "Fetches a list of users with support for pagination via limit and offset.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List Users", + "parameters": [ + { + "type": "string", + "description": "Maximum number of users to return (default 0)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Number of users to skip", + "name": "offset", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserListResult" + } + }, + "400": { + "description": "Invalid Input (Format Error)", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Search Engine or Missing Offset)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/update": { + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).\nValidation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update User Information", + "parameters": [ + { + "description": "Updated User Profile Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "400": { + "description": "Invalid Input (Validation Failed)", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Database Error / UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "data.User": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "data.UserSearchDoc": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "exception.Builder": { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + }, + "error": {}, + "errorCode": { + "type": "string" + }, + "original": { + "type": "string" + }, + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service_auth.ExchangeData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "service_auth.ExchangeResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "redirect_uri": { + "type": "string" + } + } + } + } + }, + "service_auth.MagicData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "email": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + }, + "turnstile_token": { + "type": "string" + } + } + }, + "service_auth.MagicResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": {} + } + }, + "service_auth.RefreshData": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.RefreshResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_auth.TokenData": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "service_auth.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.TokenResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_event.CheckinQueryResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_at": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinSubmitData": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + }, + "service_event.CheckinSubmitResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + } + } + }, + "service_event.InfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "end_time": { + "type": "string" + }, + "name": { + "type": "string" + }, + "start_time": { + "type": "string" + } + } + } + } + }, + "service_user.UserInfoData": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service_user.UserInfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + }, + "service_user.UserListResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_list": { + "type": "array", + "items": { + "$ref": "#/definitions/data.UserSearchDoc" + } + } + } + }, + "service_user.UserTableResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_table": { + "type": "array", + "items": { + "$ref": "#/definitions/data.User" + } + } + } + }, + "shared.CommonResult": { + "type": "object", + "properties": { + "exception": { + "$ref": "#/definitions/exception.Builder" + }, + "httpCode": { + "type": "integer" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..d3e5f8d --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,979 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/auth/exchange": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Exchanges client credentials and user session for a specific redirect authorization code.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Auth Code", + "parameters": [ + { + "description": "Exchange Request Credentials", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.ExchangeData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.ExchangeResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/magic": { + "post": { + "description": "Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Request Magic Link", + "parameters": [ + { + "description": "Magic Link Request Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.MagicData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.MagicResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Turnstile Verification Failed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/redirect": { + "get": { + "description": "Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "text/html" + ], + "tags": [ + "Authentication" + ], + "summary": "Handle Auth Callback and Redirect", + "parameters": [ + { + "type": "string", + "description": "Client Identifier", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Target Redirect URI", + "name": "redirect_uri", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Temporary Verification Code", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Opaque state used to maintain state between the request and callback", + "name": "state", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Redirect to the provided RedirectUri with a new code", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid Input / Client Not Found / URI Mismatch", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Verification Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Accepts a valid refresh token to issue a new access token and a rotated refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh Access Token", + "parameters": [ + { + "description": "Refresh Token Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.RefreshData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.RefreshResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Invalid Refresh Token", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/token": { + "post": { + "description": "Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Code for Token", + "parameters": [ + { + "description": "Token Request Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.TokenData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.TokenResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/event/checkin": { + "get": { + "description": "Creates a temporary check-in code for the authenticated user and event.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Generate Check-in Code", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinResult" + } + } + } + } + }, + "/event/checkin/query": { + "get": { + "description": "Returns the timestamp of when the user checked in, or null if not yet checked in.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Query Check-in Status", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinQueryResult" + } + } + } + } + }, + "/event/checkin/submit": { + "post": { + "description": "Submits the generated code to mark the user as attended.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Submit Check-in Code", + "parameters": [ + { + "description": "Checkin Code Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitResult" + } + } + } + } + }, + "/event/info": { + "get": { + "description": "Fetches the name, start time, and end time of an event using its UUID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Get Event Information", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.InfoResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Event Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/full": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches all user records without pagination. This is typically used for administrative overview or data export.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get Full User Table", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserTableResult" + } + }, + "500": { + "description": "Internal Server Error (Database Error)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the complete profile data for the user associated with the provided session/token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get My User Information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "User Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/list": { + "get": { + "description": "Fetches a list of users with support for pagination via limit and offset.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List Users", + "parameters": [ + { + "type": "string", + "description": "Maximum number of users to return (default 0)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Number of users to skip", + "name": "offset", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserListResult" + } + }, + "400": { + "description": "Invalid Input (Format Error)", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Search Engine or Missing Offset)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/update": { + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).\nValidation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update User Information", + "parameters": [ + { + "description": "Updated User Profile Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "400": { + "description": "Invalid Input (Validation Failed)", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Database Error / UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "data.User": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "data.UserSearchDoc": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "exception.Builder": { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + }, + "error": {}, + "errorCode": { + "type": "string" + }, + "original": { + "type": "string" + }, + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service_auth.ExchangeData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "service_auth.ExchangeResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "redirect_uri": { + "type": "string" + } + } + } + } + }, + "service_auth.MagicData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "email": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + }, + "turnstile_token": { + "type": "string" + } + } + }, + "service_auth.MagicResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": {} + } + }, + "service_auth.RefreshData": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.RefreshResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_auth.TokenData": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "service_auth.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.TokenResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_event.CheckinQueryResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_at": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinSubmitData": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + }, + "service_event.CheckinSubmitResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + } + } + }, + "service_event.InfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "end_time": { + "type": "string" + }, + "name": { + "type": "string" + }, + "start_time": { + "type": "string" + } + } + } + } + }, + "service_user.UserInfoData": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service_user.UserInfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + }, + "service_user.UserListResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_list": { + "type": "array", + "items": { + "$ref": "#/definitions/data.UserSearchDoc" + } + } + } + }, + "service_user.UserTableResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_table": { + "type": "array", + "items": { + "$ref": "#/definitions/data.User" + } + } + } + }, + "shared.CommonResult": { + "type": "object", + "properties": { + "exception": { + "$ref": "#/definitions/exception.Builder" + }, + "httpCode": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..77f74d9 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,646 @@ +definitions: + data.User: + properties: + allow_public: + type: boolean + avatar: + type: string + bio: + type: string + email: + type: string + id: + type: integer + nickname: + type: string + permission_level: + type: integer + subtitle: + type: string + user_id: + type: string + username: + type: string + uuid: + type: string + type: object + data.UserSearchDoc: + properties: + avatar: + type: string + email: + type: string + nickname: + type: string + subtitle: + type: string + type: + type: string + user_id: + type: string + username: + type: string + type: object + exception.Builder: + properties: + endpoint: + type: string + error: {} + errorCode: + type: string + original: + type: string + service: + type: string + status: + type: string + type: + type: string + type: object + service_auth.ExchangeData: + properties: + client_id: + type: string + redirect_uri: + type: string + state: + type: string + type: object + service_auth.ExchangeResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + redirect_uri: + type: string + type: object + type: object + service_auth.MagicData: + properties: + client_id: + type: string + client_ip: + type: string + email: + type: string + redirect_uri: + type: string + state: + type: string + turnstile_token: + type: string + type: object + service_auth.MagicResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: {} + type: object + service_auth.RefreshData: + properties: + refresh_token: + type: string + type: object + service_auth.RefreshResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + $ref: '#/definitions/service_auth.TokenResponse' + type: object + service_auth.TokenData: + properties: + code: + type: string + type: object + service_auth.TokenResponse: + properties: + access_token: + type: string + refresh_token: + type: string + type: object + service_auth.TokenResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + $ref: '#/definitions/service_auth.TokenResponse' + type: object + service_event.CheckinQueryResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + checkin_at: + type: string + type: object + type: object + service_event.CheckinResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + checkin_code: + type: string + type: object + type: object + service_event.CheckinSubmitData: + properties: + checkin_code: + type: string + type: object + service_event.CheckinSubmitResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + type: object + service_event.InfoResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + end_time: + type: string + name: + type: string + start_time: + type: string + type: object + type: object + service_user.UserInfoData: + properties: + allow_public: + type: boolean + avatar: + type: string + bio: + type: string + email: + type: string + nickname: + type: string + permission_level: + type: integer + subtitle: + type: string + user_id: + type: string + username: + type: string + type: object + service_user.UserInfoResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + $ref: '#/definitions/service_user.UserInfoData' + type: object + service_user.UserListResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + user_list: + items: + $ref: '#/definitions/data.UserSearchDoc' + type: array + type: object + service_user.UserTableResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + user_table: + items: + $ref: '#/definitions/data.User' + type: array + type: object + shared.CommonResult: + properties: + exception: + $ref: '#/definitions/exception.Builder' + httpCode: + type: integer + type: object +info: + contact: {} +paths: + /auth/exchange: + post: + consumes: + - application/json + description: Exchanges client credentials and user session for a specific redirect + authorization code. + parameters: + - description: Exchange Request Credentials + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.ExchangeData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.ExchangeResult' + "400": + description: Invalid Input + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Exchange Auth Code + tags: + - Authentication + /auth/magic: + post: + consumes: + - application/json + description: Verifies Turnstile token and sends an authentication link via email. + Returns the URI directly if debug mode is enabled. + parameters: + - description: Magic Link Request Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.MagicData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.MagicResult' + "400": + description: Invalid Input + schema: + type: string + "403": + description: Turnstile Verification Failed + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Request Magic Link + tags: + - Authentication + /auth/redirect: + get: + consumes: + - application/x-www-form-urlencoded + description: Verifies the temporary email code, ensures the user exists (or + creates one), validates the client's redirect URI, and finally performs a + 302 redirect with a new authorization code. + parameters: + - description: Client Identifier + in: query + name: client_id + required: true + type: string + - description: Target Redirect URI + in: query + name: redirect_uri + required: true + type: string + - description: Temporary Verification Code + in: query + name: code + required: true + type: string + - description: Opaque state used to maintain state between the request and callback + in: query + name: state + type: string + produces: + - text/html + responses: + "302": + description: Redirect to the provided RedirectUri with a new code + schema: + type: string + "400": + description: Invalid Input / Client Not Found / URI Mismatch + schema: + type: string + "403": + description: Invalid or Expired Verification Code + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Handle Auth Callback and Redirect + tags: + - Authentication + /auth/refresh: + post: + consumes: + - application/json + description: Accepts a valid refresh token to issue a new access token and a + rotated refresh token. + parameters: + - description: Refresh Token Body + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.RefreshData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.RefreshResult' + "400": + description: Invalid Input + schema: + type: string + "401": + description: Invalid Refresh Token + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Refresh Access Token + tags: + - Authentication + /auth/token: + post: + consumes: + - application/json + description: Verifies the provided authorization code and issues a pair of JWT + tokens (Access and Refresh). + parameters: + - description: Token Request Body + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.TokenData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.TokenResult' + "400": + description: Invalid Input + schema: + type: string + "403": + description: Invalid or Expired Code + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Exchange Code for Token + tags: + - Authentication + /event/checkin: + get: + consumes: + - application/json + description: Creates a temporary check-in code for the authenticated user and + event. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.CheckinResult' + summary: Generate Check-in Code + tags: + - Event + /event/checkin/query: + get: + consumes: + - application/json + description: Returns the timestamp of when the user checked in, or null if not + yet checked in. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.CheckinQueryResult' + summary: Query Check-in Status + tags: + - Event + /event/checkin/submit: + post: + consumes: + - application/json + description: Submits the generated code to mark the user as attended. + parameters: + - description: Checkin Code Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_event.CheckinSubmitData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.CheckinSubmitResult' + summary: Submit Check-in Code + tags: + - Event + /event/info: + get: + consumes: + - application/json + description: Fetches the name, start time, and end time of an event using its + UUID. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.InfoResult' + "400": + description: Invalid Input + schema: + type: string + "404": + description: Event Not Found + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Get Event Information + tags: + - Event + /user/full: + get: + consumes: + - application/json + description: Fetches all user records without pagination. This is typically + used for administrative overview or data export. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserTableResult' + "500": + description: Internal Server Error (Database Error) + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Get Full User Table + tags: + - User + /user/info: + get: + consumes: + - application/json + description: Fetches the complete profile data for the user associated with + the provided session/token. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserInfoResult' + "403": + description: Missing User ID / Unauthorized + schema: + type: string + "404": + description: User Not Found + schema: + type: string + "500": + description: Internal Server Error (UUID Parse Failed) + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Get My User Information + tags: + - User + /user/list: + get: + consumes: + - application/json + description: Fetches a list of users with support for pagination via limit and + offset. + parameters: + - description: Maximum number of users to return (default 0) + in: query + name: limit + type: string + - description: Number of users to skip + in: query + name: offset + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserListResult' + "400": + description: Invalid Input (Format Error) + schema: + type: string + "500": + description: Internal Server Error (Search Engine or Missing Offset) + schema: + type: string + summary: List Users + tags: + - User + /user/update: + patch: + consumes: + - application/json + description: |- + Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). + Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). + parameters: + - description: Updated User Profile Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_user.UserInfoData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserInfoResult' + "400": + description: Invalid Input (Validation Failed) + schema: + type: string + "403": + description: Missing User ID / Unauthorized + schema: + type: string + "500": + description: Internal Server Error (Database Error / UUID Parse Failed) + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Update User Information + tags: + - User +swagger: "2.0" diff --git a/go.mod b/go.mod index c345d84..a14de4b 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( 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.47.0 golang.org/x/text v0.33.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 @@ -43,13 +43,16 @@ require ( 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/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // 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/openapi-util v0.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.2 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect @@ -63,11 +66,22 @@ require ( 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-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-playground/locales v0.14.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.30.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/goccy/go-yaml v1.19.1 // indirect + github.com/goccy/go-yaml v1.19.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 @@ -77,10 +91,12 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // 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/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.1 // 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/reflect2 v1.0.2 // indirect @@ -89,7 +105,7 @@ require ( 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/quic-go v0.57.1 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/segmentio/asm v1.2.0 // indirect @@ -99,6 +115,9 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.6.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect @@ -108,15 +127,18 @@ require ( go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/tools v0.41.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 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.5.7 // indirect ) diff --git a/go.sum b/go.sum index 9901b74..56b821d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,14 @@ github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeE 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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 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-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= @@ -63,8 +71,12 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= 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.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.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= @@ -76,6 +88,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -105,6 +118,38 @@ 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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= 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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -113,6 +158,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk= github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -122,6 +169,8 @@ 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-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.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/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/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -176,6 +225,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -196,6 +247,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -229,6 +286,8 @@ 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/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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= 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= @@ -273,6 +332,14 @@ 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= 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.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= @@ -355,6 +422,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -366,6 +435,8 @@ 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.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -377,6 +448,7 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/ 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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -391,6 +463,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -413,6 +487,7 @@ 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-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-20210420072515-93ed5bcd2bfe/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -429,6 +504,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -454,6 +531,7 @@ 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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -470,6 +548,10 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -514,7 +596,10 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= diff --git a/internal/ali_cnrid/kyc.go b/internal/ali_cnrid/kyc.go index 228ca16..56baa46 100644 --- a/internal/ali_cnrid/kyc.go +++ b/internal/ali_cnrid/kyc.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "nixcn-cms/internal/cryptography" + "nixcn-cms/internal/kyc" "unicode/utf8" alicloudauth20190307 "github.com/alibabacloud-go/cloudauth-20190307/v4/client" @@ -18,21 +19,21 @@ import ( "github.com/spf13/viper" ) -func DecodeB64Json(b64Json string) (*KycInfo, error) { +func DecodeB64Json(b64Json string) (*kyc.KycInfo, error) { rawJson, err := base64.StdEncoding.DecodeString(b64Json) if err != nil { return nil, errors.New("[KYC] invalid base64 json") } - var kyc KycInfo - if err := json.Unmarshal(rawJson, &kyc); err != nil { + var kycInfo kyc.KycInfo + if err := json.Unmarshal(rawJson, &kycInfo); err != nil { return nil, errors.New("[KYC] invalid json structure") } - return &kyc, nil + return &kycInfo, nil } -func EncodeAES(kyc *KycInfo) (*string, error) { +func EncodeAES(kyc *kyc.KycInfo) (*string, error) { plainJson, err := json.Marshal(kyc) if err != nil { return nil, err @@ -47,22 +48,22 @@ func EncodeAES(kyc *KycInfo) (*string, error) { return &encrypted, nil } -func DecodeAES(cipherStr string) (*KycInfo, error) { +func DecodeAES(cipherStr string) (*kyc.KycInfo, error) { aesKey := viper.GetString("secrets.kyc_info_key") plainBytes, err := cryptography.AESCBCDecrypt(cipherStr, []byte(aesKey)) if err != nil { return nil, err } - var kyc KycInfo - if err := json.Unmarshal(plainBytes, &kyc); err != nil { + var kycInfo kyc.KycInfo + if err := json.Unmarshal(plainBytes, &kycInfo); err != nil { return nil, errors.New("[KYC] invalid decrypted json") } - return &kyc, nil + return &kycInfo, nil } -func MD5AliEnc(kyc *KycInfo) (*KycAli, error) { +func MD5AliEnc(kyc *kyc.KycInfo) (*KycAli, error) { if kyc.Type != "Chinese" { return nil, nil } diff --git a/server/server.go b/server/server.go index 9bbff23..52a547a 100644 --- a/server/server.go +++ b/server/server.go @@ -11,9 +11,23 @@ import ( "github.com/gin-gonic/gin" "github.com/spf13/viper" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" ) +// @title NixCN CMS API +// @version 1.0 +// @description API Docs based on Gin framework +// @termsOfService http://swagger.io/terms/ +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:8080 +// @BasePath /api/v1 +// @schemes http https func Start(ctx context.Context) { if !viper.GetBool("server.debug_mode") { gin.SetMode(gin.ReleaseMode) @@ -25,6 +39,8 @@ func Start(ctx context.Context) { r.Use(middleware.GinLogger()) r.Use(gin.Recovery()) + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + api.Handler(r.Group("/api/v1")) // Start http server diff --git a/service/service_auth/redirect.go b/service/service_auth/redirect.go index 4b612fb..7174386 100644 --- a/service/service_auth/redirect.go +++ b/service/service_auth/redirect.go @@ -2,7 +2,14 @@ package service_auth import ( "context" + "net/url" + "nixcn-cms/data" + "nixcn-cms/internal/authcode" + "nixcn-cms/internal/exception" "nixcn-cms/service/shared" + + "github.com/google/uuid" + "gorm.io/gorm" ) type RedirectData struct { @@ -23,5 +30,181 @@ type RedirectResult struct { } func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *RedirectResult) { + var err error + authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code) + if !ok { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectTokenInvalid). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 403, + Exception: exception, + }, + } + + return + } + + userData, err := new(data.User). + GetByEmail(payload.Context, &authCode.Email) + if err != nil { + if err == gorm.ErrRecordNotFound { + userData.UUID = uuid.New() + userData.UserId = uuid.New() + userData.Email = authCode.Email + userData.Username = userData.UserId.String() + userData.PermissionLevel = 10 + if err := userData.Create(payload.Context); err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + } else { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + } + + clientData := new(data.Client) + client, err := clientData.GetClientByClientId(payload.Context, payload.Data.ClientId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectClientNotFound). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + if err = client.ValidateRedirectURI(payload.Data.RedirectUri); err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectUriMismatch). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + newCode, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, authCode.Email) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + targetUrl, err := url.Parse(payload.Data.RedirectUri) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectInvalidUri). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + query := targetUrl.Query() + query.Set("code", newCode) + if payload.Data.State != "" { + query.Set("state", payload.Data.State) + } + targetUrl.RawQuery = query.Encode() + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: targetUrl.String(), + } + + return } diff --git a/service/service_auth/refresh.go b/service/service_auth/refresh.go new file mode 100644 index 0000000..f04d847 --- /dev/null +++ b/service/service_auth/refresh.go @@ -0,0 +1,99 @@ +package service_auth + +import ( + "context" + "nixcn-cms/internal/authtoken" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + + "github.com/spf13/viper" +) + +type RefreshData struct { + RefreshToken string `json:"refresh_token"` +} + +type RefreshPayload struct { + Context context.Context + Data *RefreshData +} + +type RefreshResult struct { + Common shared.CommonResult + Data *TokenResponse +} + +func (self *AuthServiceImpl) Refresh(payload *RefreshPayload) (result *RefreshResult) { + JwtTool := authtoken.Token{ + Application: viper.GetString("server.application"), + } + + // 1. Refresh Access Token + accessToken, err := JwtTool.RefreshAccessToken(payload.Context, payload.Data.RefreshToken) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRefreshInvalidToken). + SetError(err). + Throw(payload.Context) + + result = &RefreshResult{ + Common: shared.CommonResult{ + HttpCode: 401, + Exception: exception, + }, + Data: nil, + } + + return + } + + // 2. Renew Refresh Token (Rotation) + refreshToken, err := JwtTool.RenewRefreshToken(payload.Context, payload.Data.RefreshToken) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRefreshRenewFailed). + SetError(err). + Throw(payload.Context) + + result = &RefreshResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + // 3. Success Assignment + exception := new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &RefreshResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &TokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, + } + + return +} diff --git a/service/service_auth/service.go b/service/service_auth/service.go index 423aa86..8bc5beb 100644 --- a/service/service_auth/service.go +++ b/service/service_auth/service.go @@ -3,6 +3,9 @@ package service_auth type AuthService interface { Exchange(*ExchangePayload) *ExchangeResult Magic(*MagicPayload) *MagicResult + Redirect(*RedirectPayload) *RedirectResult + Token(*TokenPayload) *TokenResult + Refresh(*RefreshPayload) *RefreshResult } type AuthServiceImpl struct{} diff --git a/service/service_auth/token.go b/service/service_auth/token.go new file mode 100644 index 0000000..0e5fd14 --- /dev/null +++ b/service/service_auth/token.go @@ -0,0 +1,118 @@ +package service_auth + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/authcode" + "nixcn-cms/internal/authtoken" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + + "github.com/spf13/viper" +) + +type TokenData struct { + Code string `json:"code"` +} + +type TokenPayload struct { + Context context.Context + Data *TokenData +} + +type TokenResult struct { + Common shared.CommonResult + Data *TokenResponse +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func (self *AuthServiceImpl) Token(payload *TokenPayload) (result *TokenResult) { + authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code) + if !ok { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthTokenInvalidToken). + Throw(payload.Context) + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 403, + Exception: exception, + }, + } + + return + } + + userData := new(data.User) + user, err := userData.GetByEmail(payload.Context, &authCode.Email) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + JwtTool := authtoken.Token{ + Application: viper.GetString("server.application"), + } + accessToken, refreshToken, err := JwtTool.IssueTokens(payload.Context, authCode.ClientId, user.UserId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthTokenGenFailed). + SetError(err). + Throw(payload.Context) + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &TokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, + } + + return +} diff --git a/service/service_event/checkin.go b/service/service_event/checkin.go new file mode 100644 index 0000000..7079554 --- /dev/null +++ b/service/service_event/checkin.go @@ -0,0 +1,191 @@ +package service_event + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + "time" + + "github.com/google/uuid" +) + +type CheckinData struct { + EventId uuid.UUID `json:"event_id"` +} + +type CheckinPayload struct { + Context context.Context + UserId uuid.UUID + Data *CheckinData +} + +type CheckinResult struct { + Common shared.CommonResult + Data *struct { + CheckinCode *string `json:"checkin_code"` + } +} + +func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) { + attendance := &data.Attendance{UserId: payload.UserId} + code, err := attendance.GenCheckinCode(payload.Context, payload.Data.EventId) + if err != nil { + result = &CheckinResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventCheckinGenCodeFailed). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + result = &CheckinResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &struct { + CheckinCode *string `json:"checkin_code"` + }{code}, + } + return +} + +type CheckinSubmitData struct { + CheckinCode string `json:"checkin_code"` +} + +type CheckinSubmitPayload struct { + Context context.Context + Data *CheckinSubmitData +} + +type CheckinSubmitResult struct { + Common shared.CommonResult +} + +func (self *EventServiceImpl) CheckinSubmit(payload *CheckinSubmitPayload) (result *CheckinSubmitResult) { + attendanceData := new(data.Attendance) + err := attendanceData.VerifyCheckinCode(payload.Context, payload.Data.CheckinCode) + if err != nil { + result = &CheckinSubmitResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinSubmit). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + result = &CheckinSubmitResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinSubmit). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + } + return +} + +type CheckinQueryData struct { + EventId uuid.UUID `json:"event_id"` +} + +type CheckinQueryPayload struct { + Context context.Context + UserId uuid.UUID + Data *CheckinQueryData +} + +type CheckinQueryResult struct { + Common shared.CommonResult + Data *struct { + CheckinAt *time.Time `json:"checkin_at"` + } +} + +func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result *CheckinQueryResult) { + attendanceData := new(data.Attendance) + attendance, err := attendanceData.GetAttendance(payload.Context, payload.UserId, payload.Data.EventId) + + if err != nil { + result = &CheckinQueryResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + if attendance == nil { + result = &CheckinQueryResult{ + Common: shared.CommonResult{ + HttpCode: 404, + Exception: new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventCheckinQueryRecordNotFound). + Throw(payload.Context), + }, + } + return + } + + var checkinAt *time.Time + if !attendance.CheckinAt.IsZero() { + checkinAt = &attendance.CheckinAt + } + + result = &CheckinQueryResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &struct { + CheckinAt *time.Time `json:"checkin_at"` + }{checkinAt}, + } + return +} diff --git a/service/service_event/info.go b/service/service_event/info.go new file mode 100644 index 0000000..743fb21 --- /dev/null +++ b/service/service_event/info.go @@ -0,0 +1,78 @@ +package service_event + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + "time" + + "github.com/google/uuid" +) + +type InfoData struct { + EventId uuid.UUID `json:"event_id"` +} + +type InfoPayload struct { + Context context.Context + Data *InfoData +} + +type InfoResult struct { + Common shared.CommonResult + Data *struct { + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + } +} + +func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) { + event, err := new(data.Event).GetEventById(payload.Context, payload.Data.EventId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceInfo). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventInfoNotFound). + SetError(err). + Throw(payload.Context) + + result = &InfoResult{ + Common: shared.CommonResult{ + HttpCode: 404, + Exception: exception, + }, + } + + return + } + + resultData := struct { + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + }{ + Name: event.Name, + StartTime: event.StartTime, + EndTime: event.EndTime, + } + + result = &InfoResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceInfo). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &resultData, + } + + return +} diff --git a/service/service_event/service.go b/service/service_event/service.go new file mode 100644 index 0000000..74bcf5a --- /dev/null +++ b/service/service_event/service.go @@ -0,0 +1,14 @@ +package service_event + +type EventService interface { + Checkin(*CheckinPayload) *CheckinResult + CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult + CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult + Info(*InfoPayload) *InfoResult +} + +type EventServiceImpl struct{} + +func NewEventService() EventService { + return &EventServiceImpl{} +} -- 2.49.1 From 330b037dca0df1505f2a335c08bd0f2e17c12096 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 00:58:28 +0800 Subject: [PATCH 016/195] Fix stupid ai bug Signed-off-by: Asai Neko --- api/auth/exchange.go | 8 +++--- api/auth/magic.go | 8 +++--- api/auth/redirect.go | 7 ++--- api/auth/refresh.go | 8 +++--- api/auth/token.go | 8 +++--- api/event/checkin.go | 11 +++++--- api/event/info.go | 8 +++--- api/user/full.go | 4 +-- api/user/info.go | 8 +++--- api/user/list.go | 8 +++--- api/user/update.go | 8 +++--- go.mod | 12 +++------ go.sum | 62 ++++++-------------------------------------- server/server.go | 2 ++ 14 files changed, 59 insertions(+), 103 deletions(-) diff --git a/api/auth/exchange.go b/api/auth/exchange.go index 73cc4fe..f066834 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -16,10 +16,10 @@ import ( // @Accept json // @Produce json // @Param payload body service_auth.ExchangeData true "Exchange Request Credentials" -// @Success 200 {object} service_auth.ExchangeResult -// @Failure 400 {string} string "Invalid Input" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal Server Error" +// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResult} "Successful exchange" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Security ApiKeyAuth // @Router /auth/exchange [post] func (self *AuthHandler) Exchange(c *gin.Context) { diff --git a/api/auth/magic.go b/api/auth/magic.go index 11c3528..37fe01a 100644 --- a/api/auth/magic.go +++ b/api/auth/magic.go @@ -15,10 +15,10 @@ import ( // @Accept json // @Produce json // @Param payload body service_auth.MagicData true "Magic Link Request Data" -// @Success 200 {object} service_auth.MagicResult -// @Failure 400 {string} string "Invalid Input" -// @Failure 403 {string} string "Turnstile Verification Failed" -// @Failure 500 {string} string "Internal Server Error" +// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResult} "Successful request" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /auth/magic [post] func (self *AuthHandler) Magic(c *gin.Context) { var magicData service_auth.MagicData diff --git a/api/auth/redirect.go b/api/auth/redirect.go index 8e2742b..28cfc8e 100644 --- a/api/auth/redirect.go +++ b/api/auth/redirect.go @@ -13,15 +13,16 @@ import ( // @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. // @Tags Authentication // @Accept x-www-form-urlencoded +// @Produce json // @Produce html // @Param client_id query string true "Client Identifier" // @Param redirect_uri query string true "Target Redirect URI" // @Param code query string true "Temporary Verification Code" // @Param state query string false "Opaque state used to maintain state between the request and callback" // @Success 302 {string} string "Redirect to the provided RedirectUri with a new code" -// @Failure 400 {string} string "Invalid Input / Client Not Found / URI Mismatch" -// @Failure 403 {string} string "Invalid or Expired Verification Code" -// @Failure 500 {string} string "Internal Server Error" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Client Not Found / URI Mismatch" +// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Verification Code" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /auth/redirect [get] func (self *AuthHandler) Redirect(c *gin.Context) { data := &service_auth.RedirectData{ diff --git a/api/auth/refresh.go b/api/auth/refresh.go index d6ee131..850e9f0 100644 --- a/api/auth/refresh.go +++ b/api/auth/refresh.go @@ -15,10 +15,10 @@ import ( // @Accept json // @Produce json // @Param payload body service_auth.RefreshData true "Refresh Token Body" -// @Success 200 {object} service_auth.RefreshResult -// @Failure 400 {string} string "Invalid Input" -// @Failure 401 {string} string "Invalid Refresh Token" -// @Failure 500 {string} string "Internal Server Error" +// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful rotation" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 401 {object} utils.RespStatus{data=nil} "Invalid Refresh Token" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /auth/refresh [post] func (self *AuthHandler) Refresh(c *gin.Context) { var refreshData service_auth.RefreshData diff --git a/api/auth/token.go b/api/auth/token.go index 942f322..d841766 100644 --- a/api/auth/token.go +++ b/api/auth/token.go @@ -15,10 +15,10 @@ import ( // @Accept json // @Produce json // @Param payload body service_auth.TokenData true "Token Request Body" -// @Success 200 {object} service_auth.TokenResult -// @Failure 400 {string} string "Invalid Input" -// @Failure 403 {string} string "Invalid or Expired Code" -// @Failure 500 {string} string "Internal Server Error" +// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful token issuance" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Code" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /auth/token [post] func (self *AuthHandler) Token(c *gin.Context) { var tokenData service_auth.TokenData diff --git a/api/event/checkin.go b/api/event/checkin.go index f980662..3465824 100644 --- a/api/event/checkin.go +++ b/api/event/checkin.go @@ -16,7 +16,9 @@ import ( // @Accept json // @Produce json // @Param event_id query string true "Event UUID" -// @Success 200 {object} service_event.CheckinResult +// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResult} "Successfully generated code" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /event/checkin [get] func (self *EventHandler) Checkin(c *gin.Context) { userIdOrig, _ := c.Get("user_id") @@ -52,7 +54,8 @@ func (self *EventHandler) Checkin(c *gin.Context) { // @Accept json // @Produce json // @Param payload body service_event.CheckinSubmitData true "Checkin Code Data" -// @Success 200 {object} service_event.CheckinSubmitResult +// @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input" // @Router /event/checkin/submit [post] func (self *EventHandler) CheckinSubmit(c *gin.Context) { var data service_event.CheckinSubmitData @@ -83,7 +86,9 @@ func (self *EventHandler) CheckinSubmit(c *gin.Context) { // @Accept json // @Produce json // @Param event_id query string true "Event UUID" -// @Success 200 {object} service_event.CheckinQueryResult +// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResult} "Current attendance status" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found" // @Router /event/checkin/query [get] func (self *EventHandler) CheckinQuery(c *gin.Context) { userIdOrig, _ := c.Get("user_id") diff --git a/api/event/info.go b/api/event/info.go index 0695d3a..b5246b6 100644 --- a/api/event/info.go +++ b/api/event/info.go @@ -16,10 +16,10 @@ import ( // @Accept json // @Produce json // @Param event_id query string true "Event UUID" -// @Success 200 {object} service_event.InfoResult -// @Failure 400 {string} string "Invalid Input" -// @Failure 404 {string} string "Event Not Found" -// @Failure 500 {string} string "Internal Server Error" +// @Success 200 {object} utils.RespStatus{data=service_event.InfoResult} "Successful retrieval" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /event/info [get] func (self *EventHandler) Info(c *gin.Context) { eventIdOrig := c.Query("event_id") diff --git a/api/user/full.go b/api/user/full.go index c5a2440..7097486 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -14,8 +14,8 @@ import ( // @Tags User // @Accept json // @Produce json -// @Success 200 {object} service_user.UserTableResult -// @Failure 500 {string} string "Internal Server Error (Database Error)" +// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResult} "Successful retrieval of full user table" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error)" // @Security ApiKeyAuth // @Router /user/full [get] func (self *UserHandler) Full(c *gin.Context) { diff --git a/api/user/info.go b/api/user/info.go index bcf21af..faeeac1 100644 --- a/api/user/info.go +++ b/api/user/info.go @@ -15,10 +15,10 @@ import ( // @Tags User // @Accept json // @Produce json -// @Success 200 {object} service_user.UserInfoResult -// @Failure 403 {string} string "Missing User ID / Unauthorized" -// @Failure 404 {string} string "User Not Found" -// @Failure 500 {string} string "Internal Server Error (UUID Parse Failed)" +// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval" +// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized" +// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)" // @Security ApiKeyAuth // @Router /user/info [get] func (self *UserHandler) Info(c *gin.Context) { diff --git a/api/user/list.go b/api/user/list.go index 48c9934..08478bc 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -10,15 +10,15 @@ import ( // List retrieves a paginated list of users from the search engine. // @Summary List Users -// @Description Fetches a list of users with support for pagination via limit and offset. +// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. // @Tags User // @Accept json // @Produce json // @Param limit query string false "Maximum number of users to return (default 0)" // @Param offset query string true "Number of users to skip" -// @Success 200 {object} service_user.UserListResult -// @Failure 400 {string} string "Invalid Input (Format Error)" -// @Failure 500 {string} string "Internal Server Error (Search Engine or Missing Offset)" +// @Success 200 {object} utils.RespStatus{data=[]data.UserSearchDoc} "Successful paginated list retrieval" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)" // @Router /user/list [get] func (self *UserHandler) List(c *gin.Context) { type ListQuery struct { diff --git a/api/user/update.go b/api/user/update.go index ec34c1a..e525c9d 100644 --- a/api/user/update.go +++ b/api/user/update.go @@ -17,10 +17,10 @@ import ( // @Accept json // @Produce json // @Param payload body service_user.UserInfoData true "Updated User Profile Data" -// @Success 200 {object} service_user.UserInfoResult -// @Failure 400 {string} string "Invalid Input (Validation Failed)" -// @Failure 403 {string} string "Missing User ID / Unauthorized" -// @Failure 500 {string} string "Internal Server Error (Database Error / UUID Parse Failed)" +// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)" +// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)" // @Security ApiKeyAuth // @Router /user/update [patch] func (self *UserHandler) Update(c *gin.Context) { diff --git a/go.mod b/go.mod index a14de4b..e98cf52 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,9 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.17.2 github.com/redis/go-redis/v9 v9.17.2 github.com/spf13/viper v1.21.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 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 @@ -44,8 +47,6 @@ require ( github.com/ClickHouse/ch-go v0.61.5 // indirect github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.2.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // 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/openapi-util v0.1.1 // indirect @@ -69,7 +70,6 @@ require ( github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/spec v0.22.3 // indirect - github.com/go-openapi/swag v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect @@ -91,12 +91,10 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/josharian/intern v1.0.0 // 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/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.9.1 // 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/reflect2 v1.0.2 // indirect @@ -115,9 +113,6 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/files v1.0.1 // indirect - github.com/swaggo/gin-swagger v1.6.1 // indirect - github.com/swaggo/swag v1.16.6 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect @@ -138,7 +133,6 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.5.7 // indirect ) diff --git a/go.sum b/go.sum index 56b821d..dedcc2d 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,6 @@ github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= -github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 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-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= @@ -69,12 +63,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -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.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -88,7 +78,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -105,6 +94,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 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-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -118,30 +109,21 @@ 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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= -github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= @@ -150,14 +132,16 @@ github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv4 github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk= -github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= @@ -167,8 +151,6 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 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-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.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -225,8 +207,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -247,12 +227,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -284,8 +258,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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/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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 h1:KYWnHK9pwzOUo3sNJlNmzRwZ5mw7opugn8njtGThKNg= @@ -336,8 +308,6 @@ github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= -github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= -github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -420,8 +390,6 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -448,7 +416,6 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/ 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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -461,8 +428,6 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -487,7 +452,6 @@ 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-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-20210420072515-93ed5bcd2bfe/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -502,8 +466,6 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= @@ -531,9 +493,6 @@ 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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -548,8 +507,6 @@ 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -596,10 +553,7 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= diff --git a/server/server.go b/server/server.go index 52a547a..6aa36f8 100644 --- a/server/server.go +++ b/server/server.go @@ -14,6 +14,8 @@ import ( swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + + _ "nixcn-cms/docs" ) // @title NixCN CMS API -- 2.49.1 From 732d9866db542db05533e65ffe94f250017ed6d7 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 00:59:49 +0800 Subject: [PATCH 017/195] Generate swagger docs Signed-off-by: Asai Neko --- docs/docs.go | 736 +++++++++++++++++++++++++++++++++++++++------- docs/swagger.json | 736 +++++++++++++++++++++++++++++++++++++++------- docs/swagger.yaml | 392 ++++++++++++++++++------ 3 files changed, 1563 insertions(+), 301 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index c235495..91a4f1f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -46,27 +46,75 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful exchange", "schema": { - "$ref": "#/definitions/service_auth.ExchangeResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.ExchangeResult" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "401": { "description": "Unauthorized", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -98,27 +146,75 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful request", "schema": { - "$ref": "#/definitions/service_auth.MagicResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.MagicResult" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Turnstile Verification Failed", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -131,6 +227,7 @@ const docTemplate = `{ "application/x-www-form-urlencoded" ], "produces": [ + "application/json", "text/html" ], "tags": [ @@ -176,19 +273,55 @@ const docTemplate = `{ "400": { "description": "Invalid Input / Client Not Found / URI Mismatch", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Invalid or Expired Verification Code", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -220,27 +353,75 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful rotation", "schema": { - "$ref": "#/definitions/service_auth.RefreshResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "401": { "description": "Invalid Refresh Token", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -272,27 +453,75 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful token issuance", "schema": { - "$ref": "#/definitions/service_auth.TokenResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Invalid or Expired Code", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -322,9 +551,57 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successfully generated code", "schema": { - "$ref": "#/definitions/service_event.CheckinResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_event.CheckinResult" + } + } + } + ] + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -354,9 +631,57 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Current attendance status", "schema": { - "$ref": "#/definitions/service_event.CheckinQueryResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_event.CheckinQueryResult" + } + } + } + ] + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "404": { + "description": "Record Not Found", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -388,9 +713,39 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Attendance marked successfully", "schema": { - "$ref": "#/definitions/service_event.CheckinSubmitResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "400": { + "description": "Invalid Code or Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -420,27 +775,75 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful retrieval", "schema": { - "$ref": "#/definitions/service_event.InfoResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_event.InfoResult" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "404": { "description": "Event Not Found", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -466,15 +869,39 @@ const docTemplate = `{ "summary": "Get Full User Table", "responses": { "200": { - "description": "OK", + "description": "Successful retrieval of full user table", "schema": { - "$ref": "#/definitions/service_user.UserTableResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_user.UserTableResult" + } + } + } + ] } }, "500": { "description": "Internal Server Error (Database Error)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -500,27 +927,75 @@ const docTemplate = `{ "summary": "Get My User Information", "responses": { "200": { - "description": "OK", + "description": "Successful profile retrieval", "schema": { - "$ref": "#/definitions/service_user.UserInfoResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + } + ] } }, "403": { "description": "Missing User ID / Unauthorized", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "404": { "description": "User Not Found", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error (UUID Parse Failed)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -528,7 +1003,7 @@ const docTemplate = `{ }, "/user/list": { "get": { - "description": "Fetches a list of users with support for pagination via limit and offset.", + "description": "Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.", "consumes": [ "application/json" ], @@ -556,21 +1031,60 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful paginated list retrieval", "schema": { - "$ref": "#/definitions/service_user.UserListResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/data.UserSearchDoc" + } + } + } + } + ] } }, "400": { "description": "Invalid Input (Format Error)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error (Search Engine or Missing Offset)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -607,27 +1121,75 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Successful profile update", "schema": { - "$ref": "#/definitions/service_user.UserInfoResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "400": { "description": "Invalid Input (Validation Failed)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Missing User ID / Unauthorized", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error (Database Error / UUID Parse Failed)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -793,17 +1355,6 @@ const docTemplate = `{ } } }, - "service_auth.RefreshResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "$ref": "#/definitions/service_auth.TokenResponse" - } - } - }, "service_auth.TokenData": { "type": "object", "properties": { @@ -823,17 +1374,6 @@ const docTemplate = `{ } } }, - "service_auth.TokenResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "$ref": "#/definitions/service_auth.TokenResponse" - } - } - }, "service_event.CheckinQueryResult": { "type": "object", "properties": { @@ -874,14 +1414,6 @@ const docTemplate = `{ } } }, - "service_event.CheckinSubmitResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - } - } - }, "service_event.InfoResult": { "type": "object", "properties": { @@ -936,31 +1468,6 @@ const docTemplate = `{ } } }, - "service_user.UserInfoResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "$ref": "#/definitions/service_user.UserInfoData" - } - } - }, - "service_user.UserListResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "user_list": { - "type": "array", - "items": { - "$ref": "#/definitions/data.UserSearchDoc" - } - } - } - }, "service_user.UserTableResult": { "type": "object", "properties": { @@ -985,6 +1492,21 @@ const docTemplate = `{ "type": "integer" } } + }, + "utils.RespStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "error_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index d3e5f8d..059f864 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -35,27 +35,75 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful exchange", "schema": { - "$ref": "#/definitions/service_auth.ExchangeResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.ExchangeResult" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "401": { "description": "Unauthorized", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -87,27 +135,75 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful request", "schema": { - "$ref": "#/definitions/service_auth.MagicResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.MagicResult" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Turnstile Verification Failed", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -120,6 +216,7 @@ "application/x-www-form-urlencoded" ], "produces": [ + "application/json", "text/html" ], "tags": [ @@ -165,19 +262,55 @@ "400": { "description": "Invalid Input / Client Not Found / URI Mismatch", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Invalid or Expired Verification Code", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -209,27 +342,75 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful rotation", "schema": { - "$ref": "#/definitions/service_auth.RefreshResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "401": { "description": "Invalid Refresh Token", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -261,27 +442,75 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful token issuance", "schema": { - "$ref": "#/definitions/service_auth.TokenResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Invalid or Expired Code", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -311,9 +540,57 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successfully generated code", "schema": { - "$ref": "#/definitions/service_event.CheckinResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_event.CheckinResult" + } + } + } + ] + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -343,9 +620,57 @@ ], "responses": { "200": { - "description": "OK", + "description": "Current attendance status", "schema": { - "$ref": "#/definitions/service_event.CheckinQueryResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_event.CheckinQueryResult" + } + } + } + ] + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "404": { + "description": "Record Not Found", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -377,9 +702,39 @@ ], "responses": { "200": { - "description": "OK", + "description": "Attendance marked successfully", "schema": { - "$ref": "#/definitions/service_event.CheckinSubmitResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "400": { + "description": "Invalid Code or Input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -409,27 +764,75 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful retrieval", "schema": { - "$ref": "#/definitions/service_event.InfoResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_event.InfoResult" + } + } + } + ] } }, "400": { "description": "Invalid Input", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "404": { "description": "Event Not Found", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -455,15 +858,39 @@ "summary": "Get Full User Table", "responses": { "200": { - "description": "OK", + "description": "Successful retrieval of full user table", "schema": { - "$ref": "#/definitions/service_user.UserTableResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_user.UserTableResult" + } + } + } + ] } }, "500": { "description": "Internal Server Error (Database Error)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -489,27 +916,75 @@ "summary": "Get My User Information", "responses": { "200": { - "description": "OK", + "description": "Successful profile retrieval", "schema": { - "$ref": "#/definitions/service_user.UserInfoResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + } + ] } }, "403": { "description": "Missing User ID / Unauthorized", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "404": { "description": "User Not Found", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error (UUID Parse Failed)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -517,7 +992,7 @@ }, "/user/list": { "get": { - "description": "Fetches a list of users with support for pagination via limit and offset.", + "description": "Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.", "consumes": [ "application/json" ], @@ -545,21 +1020,60 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful paginated list retrieval", "schema": { - "$ref": "#/definitions/service_user.UserListResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/data.UserSearchDoc" + } + } + } + } + ] } }, "400": { "description": "Invalid Input (Format Error)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error (Search Engine or Missing Offset)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -596,27 +1110,75 @@ ], "responses": { "200": { - "description": "OK", + "description": "Successful profile update", "schema": { - "$ref": "#/definitions/service_user.UserInfoResult" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "400": { "description": "Invalid Input (Validation Failed)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "403": { "description": "Missing User ID / Unauthorized", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } }, "500": { "description": "Internal Server Error (Database Error / UUID Parse Failed)", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] } } } @@ -782,17 +1344,6 @@ } } }, - "service_auth.RefreshResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "$ref": "#/definitions/service_auth.TokenResponse" - } - } - }, "service_auth.TokenData": { "type": "object", "properties": { @@ -812,17 +1363,6 @@ } } }, - "service_auth.TokenResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "$ref": "#/definitions/service_auth.TokenResponse" - } - } - }, "service_event.CheckinQueryResult": { "type": "object", "properties": { @@ -863,14 +1403,6 @@ } } }, - "service_event.CheckinSubmitResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - } - } - }, "service_event.InfoResult": { "type": "object", "properties": { @@ -925,31 +1457,6 @@ } } }, - "service_user.UserInfoResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "$ref": "#/definitions/service_user.UserInfoData" - } - } - }, - "service_user.UserListResult": { - "type": "object", - "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "user_list": { - "type": "array", - "items": { - "$ref": "#/definitions/data.UserSearchDoc" - } - } - } - }, "service_user.UserTableResult": { "type": "object", "properties": { @@ -974,6 +1481,21 @@ "type": "integer" } } + }, + "utils.RespStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "error_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 77f74d9..382a85b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -102,13 +102,6 @@ definitions: refresh_token: type: string type: object - service_auth.RefreshResult: - properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - $ref: '#/definitions/service_auth.TokenResponse' - type: object service_auth.TokenData: properties: code: @@ -121,13 +114,6 @@ definitions: refresh_token: type: string type: object - service_auth.TokenResult: - properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - $ref: '#/definitions/service_auth.TokenResponse' - type: object service_event.CheckinQueryResult: properties: common: @@ -153,11 +139,6 @@ definitions: checkin_code: type: string type: object - service_event.CheckinSubmitResult: - properties: - common: - $ref: '#/definitions/shared.CommonResult' - type: object service_event.InfoResult: properties: common: @@ -193,22 +174,6 @@ definitions: username: type: string type: object - service_user.UserInfoResult: - properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - $ref: '#/definitions/service_user.UserInfoData' - type: object - service_user.UserListResult: - properties: - common: - $ref: '#/definitions/shared.CommonResult' - user_list: - items: - $ref: '#/definitions/data.UserSearchDoc' - type: array - type: object service_user.UserTableResult: properties: common: @@ -225,6 +190,16 @@ definitions: httpCode: type: integer type: object + utils.RespStatus: + properties: + code: + type: integer + data: {} + error_id: + type: string + status: + type: string + type: object info: contact: {} paths: @@ -245,21 +220,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful exchange schema: - $ref: '#/definitions/service_auth.ExchangeResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_auth.ExchangeResult' + type: object "400": description: Invalid Input schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "401": description: Unauthorized schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object security: - ApiKeyAuth: [] summary: Exchange Auth Code @@ -282,21 +277,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful request schema: - $ref: '#/definitions/service_auth.MagicResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_auth.MagicResult' + type: object "400": description: Invalid Input schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "403": description: Turnstile Verification Failed schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Request Magic Link tags: - Authentication @@ -328,6 +343,7 @@ paths: name: state type: string produces: + - application/json - text/html responses: "302": @@ -337,15 +353,30 @@ paths: "400": description: Invalid Input / Client Not Found / URI Mismatch schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "403": description: Invalid or Expired Verification Code schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Handle Auth Callback and Redirect tags: - Authentication @@ -366,21 +397,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful rotation schema: - $ref: '#/definitions/service_auth.RefreshResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_auth.TokenResponse' + type: object "400": description: Invalid Input schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "401": description: Invalid Refresh Token schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Refresh Access Token tags: - Authentication @@ -401,21 +452,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful token issuance schema: - $ref: '#/definitions/service_auth.TokenResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_auth.TokenResponse' + type: object "400": description: Invalid Input schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "403": description: Invalid or Expired Code schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Exchange Code for Token tags: - Authentication @@ -435,9 +506,32 @@ paths: - application/json responses: "200": - description: OK + description: Successfully generated code schema: - $ref: '#/definitions/service_event.CheckinResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_event.CheckinResult' + type: object + "400": + description: Invalid Input + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Generate Check-in Code tags: - Event @@ -457,9 +551,32 @@ paths: - application/json responses: "200": - description: OK + description: Current attendance status schema: - $ref: '#/definitions/service_event.CheckinQueryResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_event.CheckinQueryResult' + type: object + "400": + description: Invalid Input + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "404": + description: Record Not Found + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Query Check-in Status tags: - Event @@ -479,9 +596,23 @@ paths: - application/json responses: "200": - description: OK + description: Attendance marked successfully schema: - $ref: '#/definitions/service_event.CheckinSubmitResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "400": + description: Invalid Code or Input + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Submit Check-in Code tags: - Event @@ -501,21 +632,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful retrieval schema: - $ref: '#/definitions/service_event.InfoResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_event.InfoResult' + type: object "400": description: Invalid Input schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "404": description: Event Not Found schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: Get Event Information tags: - Event @@ -529,13 +680,23 @@ paths: - application/json responses: "200": - description: OK + description: Successful retrieval of full user table schema: - $ref: '#/definitions/service_user.UserTableResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_user.UserTableResult' + type: object "500": description: Internal Server Error (Database Error) schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object security: - ApiKeyAuth: [] summary: Get Full User Table @@ -551,21 +712,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful profile retrieval schema: - $ref: '#/definitions/service_user.UserInfoResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_user.UserInfoData' + type: object "403": description: Missing User ID / Unauthorized schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "404": description: User Not Found schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error (UUID Parse Failed) schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object security: - ApiKeyAuth: [] summary: Get My User Information @@ -576,7 +757,7 @@ paths: consumes: - application/json description: Fetches a list of users with support for pagination via limit and - offset. + offset. Data is sourced from the search engine for high performance. parameters: - description: Maximum number of users to return (default 0) in: query @@ -591,17 +772,34 @@ paths: - application/json responses: "200": - description: OK + description: Successful paginated list retrieval schema: - $ref: '#/definitions/service_user.UserListResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + items: + $ref: '#/definitions/data.UserSearchDoc' + type: array + type: object "400": description: Invalid Input (Format Error) schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error (Search Engine or Missing Offset) schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object summary: List Users tags: - User @@ -623,21 +821,41 @@ paths: - application/json responses: "200": - description: OK + description: Successful profile update schema: - $ref: '#/definitions/service_user.UserInfoResult' + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "400": description: Invalid Input (Validation Failed) schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "403": description: Missing User ID / Unauthorized schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object "500": description: Internal Server Error (Database Error / UUID Parse Failed) schema: - type: string + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object security: - ApiKeyAuth: [] summary: Update User Information -- 2.49.1 From f7bde8ef2efa5e2f037551695439cb0bf1da72a0 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 01:00:51 +0800 Subject: [PATCH 018/195] Mod justfile to auto swag init when go files changed Signed-off-by: Asai Neko --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index 79e84cf..17040f5 100644 --- a/justfile +++ b/justfile @@ -39,7 +39,7 @@ gen-back: cd {{ project_dir }} && go generate . 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/**' 'swag init && go build -o {{ server_exec_path }} . && cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}' dev: clean install generate devenv up --verbose -- 2.49.1 From 654b196bfd27d5918ffdc046d76757f856511879 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 01:25:12 +0800 Subject: [PATCH 019/195] Fix swagger data struct error Signed-off-by: Asai Neko --- api/auth/exchange.go | 2 +- api/auth/magic.go | 2 +- api/event/checkin.go | 4 +- api/event/info.go | 2 +- api/user/full.go | 2 +- docs/docs.go | 127 ++++--------------- docs/swagger.json | 127 ++++--------------- docs/swagger.yaml | 98 ++++---------- generate.go | 2 + justfile | 2 +- service/service_auth/exchange.go | 14 +- service/service_auth/magic.go | 8 +- service/service_event/checkin.go | 26 ++-- service/service_event/info.go | 28 ++-- service/service_user/list_user_full_table.go | 8 +- 15 files changed, 136 insertions(+), 316 deletions(-) diff --git a/api/auth/exchange.go b/api/auth/exchange.go index f066834..4f791e6 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -16,7 +16,7 @@ import ( // @Accept json // @Produce json // @Param payload body service_auth.ExchangeData true "Exchange Request Credentials" -// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResult} "Successful exchange" +// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResponse} "Successful exchange" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" diff --git a/api/auth/magic.go b/api/auth/magic.go index 37fe01a..d54a81a 100644 --- a/api/auth/magic.go +++ b/api/auth/magic.go @@ -15,7 +15,7 @@ import ( // @Accept json // @Produce json // @Param payload body service_auth.MagicData true "Magic Link Request Data" -// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResult} "Successful request" +// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResponse} "Successful request" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" diff --git a/api/event/checkin.go b/api/event/checkin.go index 3465824..be7274b 100644 --- a/api/event/checkin.go +++ b/api/event/checkin.go @@ -16,7 +16,7 @@ import ( // @Accept json // @Produce json // @Param event_id query string true "Event UUID" -// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResult} "Successfully generated code" +// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" // @Router /event/checkin [get] @@ -86,7 +86,7 @@ func (self *EventHandler) CheckinSubmit(c *gin.Context) { // @Accept json // @Produce json // @Param event_id query string true "Event UUID" -// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResult} "Current attendance status" +// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found" // @Router /event/checkin/query [get] diff --git a/api/event/info.go b/api/event/info.go index b5246b6..ec86f6f 100644 --- a/api/event/info.go +++ b/api/event/info.go @@ -16,7 +16,7 @@ import ( // @Accept json // @Produce json // @Param event_id query string true "Event UUID" -// @Success 200 {object} utils.RespStatus{data=service_event.InfoResult} "Successful retrieval" +// @Success 200 {object} utils.RespStatus{data=service_event.InfoResponse} "Successful retrieval" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" diff --git a/api/user/full.go b/api/user/full.go index 7097486..35bf3d9 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -14,7 +14,7 @@ import ( // @Tags User // @Accept json // @Produce json -// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResult} "Successful retrieval of full user table" +// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResponse} "Successful retrieval of full user table" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error)" // @Security ApiKeyAuth // @Router /user/full [get] diff --git a/docs/docs.go b/docs/docs.go index 91a4f1f..39243cc 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -56,7 +56,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_auth.ExchangeResult" + "$ref": "#/definitions/service_auth.ExchangeResponse" } } } @@ -156,7 +156,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_auth.MagicResult" + "$ref": "#/definitions/service_auth.MagicResponse" } } } @@ -561,7 +561,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_event.CheckinResult" + "$ref": "#/definitions/service_event.CheckinResponse" } } } @@ -641,7 +641,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_event.CheckinQueryResult" + "$ref": "#/definitions/service_event.CheckinQueryResponse" } } } @@ -785,7 +785,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_event.InfoResult" + "$ref": "#/definitions/service_event.InfoResponse" } } } @@ -879,7 +879,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_user.UserTableResult" + "$ref": "#/definitions/service_user.UserTableResponse" } } } @@ -1261,30 +1261,6 @@ const docTemplate = `{ } } }, - "exception.Builder": { - "type": "object", - "properties": { - "endpoint": { - "type": "string" - }, - "error": {}, - "errorCode": { - "type": "string" - }, - "original": { - "type": "string" - }, - "service": { - "type": "string" - }, - "status": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, "service_auth.ExchangeData": { "type": "object", "properties": { @@ -1299,19 +1275,11 @@ const docTemplate = `{ } } }, - "service_auth.ExchangeResult": { + "service_auth.ExchangeResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "type": "object", - "properties": { - "redirect_uri": { - "type": "string" - } - } + "redirect_uri": { + "type": "string" } } }, @@ -1338,13 +1306,12 @@ const docTemplate = `{ } } }, - "service_auth.MagicResult": { + "service_auth.MagicResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": {} + "uri": { + "type": "string" + } } }, "service_auth.RefreshData": { @@ -1374,35 +1341,19 @@ const docTemplate = `{ } } }, - "service_event.CheckinQueryResult": { + "service_event.CheckinQueryResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "type": "object", - "properties": { - "checkin_at": { - "type": "string" - } - } + "checkin_at": { + "type": "string" } } }, - "service_event.CheckinResult": { + "service_event.CheckinResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "type": "object", - "properties": { - "checkin_code": { - "type": "string" - } - } + "checkin_code": { + "type": "string" } } }, @@ -1414,25 +1365,17 @@ const docTemplate = `{ } } }, - "service_event.InfoResult": { + "service_event.InfoResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" + "end_time": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "end_time": { - "type": "string" - }, - "name": { - "type": "string" - }, - "start_time": { - "type": "string" - } - } + "name": { + "type": "string" + }, + "start_time": { + "type": "string" } } }, @@ -1468,12 +1411,9 @@ const docTemplate = `{ } } }, - "service_user.UserTableResult": { + "service_user.UserTableResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, "user_table": { "type": "array", "items": { @@ -1482,17 +1422,6 @@ const docTemplate = `{ } } }, - "shared.CommonResult": { - "type": "object", - "properties": { - "exception": { - "$ref": "#/definitions/exception.Builder" - }, - "httpCode": { - "type": "integer" - } - } - }, "utils.RespStatus": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 059f864..8873d65 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -45,7 +45,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_auth.ExchangeResult" + "$ref": "#/definitions/service_auth.ExchangeResponse" } } } @@ -145,7 +145,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_auth.MagicResult" + "$ref": "#/definitions/service_auth.MagicResponse" } } } @@ -550,7 +550,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_event.CheckinResult" + "$ref": "#/definitions/service_event.CheckinResponse" } } } @@ -630,7 +630,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_event.CheckinQueryResult" + "$ref": "#/definitions/service_event.CheckinQueryResponse" } } } @@ -774,7 +774,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_event.InfoResult" + "$ref": "#/definitions/service_event.InfoResponse" } } } @@ -868,7 +868,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/service_user.UserTableResult" + "$ref": "#/definitions/service_user.UserTableResponse" } } } @@ -1250,30 +1250,6 @@ } } }, - "exception.Builder": { - "type": "object", - "properties": { - "endpoint": { - "type": "string" - }, - "error": {}, - "errorCode": { - "type": "string" - }, - "original": { - "type": "string" - }, - "service": { - "type": "string" - }, - "status": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, "service_auth.ExchangeData": { "type": "object", "properties": { @@ -1288,19 +1264,11 @@ } } }, - "service_auth.ExchangeResult": { + "service_auth.ExchangeResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "type": "object", - "properties": { - "redirect_uri": { - "type": "string" - } - } + "redirect_uri": { + "type": "string" } } }, @@ -1327,13 +1295,12 @@ } } }, - "service_auth.MagicResult": { + "service_auth.MagicResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": {} + "uri": { + "type": "string" + } } }, "service_auth.RefreshData": { @@ -1363,35 +1330,19 @@ } } }, - "service_event.CheckinQueryResult": { + "service_event.CheckinQueryResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "type": "object", - "properties": { - "checkin_at": { - "type": "string" - } - } + "checkin_at": { + "type": "string" } } }, - "service_event.CheckinResult": { + "service_event.CheckinResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, - "data": { - "type": "object", - "properties": { - "checkin_code": { - "type": "string" - } - } + "checkin_code": { + "type": "string" } } }, @@ -1403,25 +1354,17 @@ } } }, - "service_event.InfoResult": { + "service_event.InfoResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" + "end_time": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "end_time": { - "type": "string" - }, - "name": { - "type": "string" - }, - "start_time": { - "type": "string" - } - } + "name": { + "type": "string" + }, + "start_time": { + "type": "string" } } }, @@ -1457,12 +1400,9 @@ } } }, - "service_user.UserTableResult": { + "service_user.UserTableResponse": { "type": "object", "properties": { - "common": { - "$ref": "#/definitions/shared.CommonResult" - }, "user_table": { "type": "array", "items": { @@ -1471,17 +1411,6 @@ } } }, - "shared.CommonResult": { - "type": "object", - "properties": { - "exception": { - "$ref": "#/definitions/exception.Builder" - }, - "httpCode": { - "type": "integer" - } - } - }, "utils.RespStatus": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 382a85b..e32d97f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -41,22 +41,6 @@ definitions: username: type: string type: object - exception.Builder: - properties: - endpoint: - type: string - error: {} - errorCode: - type: string - original: - type: string - service: - type: string - status: - type: string - type: - type: string - type: object service_auth.ExchangeData: properties: client_id: @@ -66,15 +50,10 @@ definitions: state: type: string type: object - service_auth.ExchangeResult: + service_auth.ExchangeResponse: properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - properties: - redirect_uri: - type: string - type: object + redirect_uri: + type: string type: object service_auth.MagicData: properties: @@ -91,11 +70,10 @@ definitions: turnstile_token: type: string type: object - service_auth.MagicResult: + service_auth.MagicResponse: properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: {} + uri: + type: string type: object service_auth.RefreshData: properties: @@ -114,44 +92,29 @@ definitions: refresh_token: type: string type: object - service_event.CheckinQueryResult: + service_event.CheckinQueryResponse: properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - properties: - checkin_at: - type: string - type: object + checkin_at: + type: string type: object - service_event.CheckinResult: + service_event.CheckinResponse: properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - properties: - checkin_code: - type: string - type: object + checkin_code: + type: string type: object service_event.CheckinSubmitData: properties: checkin_code: type: string type: object - service_event.InfoResult: + service_event.InfoResponse: properties: - common: - $ref: '#/definitions/shared.CommonResult' - data: - properties: - end_time: - type: string - name: - type: string - start_time: - type: string - type: object + end_time: + type: string + name: + type: string + start_time: + type: string type: object service_user.UserInfoData: properties: @@ -174,22 +137,13 @@ definitions: username: type: string type: object - service_user.UserTableResult: + service_user.UserTableResponse: properties: - common: - $ref: '#/definitions/shared.CommonResult' user_table: items: $ref: '#/definitions/data.User' type: array type: object - shared.CommonResult: - properties: - exception: - $ref: '#/definitions/exception.Builder' - httpCode: - type: integer - type: object utils.RespStatus: properties: code: @@ -226,7 +180,7 @@ paths: - $ref: '#/definitions/utils.RespStatus' - properties: data: - $ref: '#/definitions/service_auth.ExchangeResult' + $ref: '#/definitions/service_auth.ExchangeResponse' type: object "400": description: Invalid Input @@ -283,7 +237,7 @@ paths: - $ref: '#/definitions/utils.RespStatus' - properties: data: - $ref: '#/definitions/service_auth.MagicResult' + $ref: '#/definitions/service_auth.MagicResponse' type: object "400": description: Invalid Input @@ -512,7 +466,7 @@ paths: - $ref: '#/definitions/utils.RespStatus' - properties: data: - $ref: '#/definitions/service_event.CheckinResult' + $ref: '#/definitions/service_event.CheckinResponse' type: object "400": description: Invalid Input @@ -557,7 +511,7 @@ paths: - $ref: '#/definitions/utils.RespStatus' - properties: data: - $ref: '#/definitions/service_event.CheckinQueryResult' + $ref: '#/definitions/service_event.CheckinQueryResponse' type: object "400": description: Invalid Input @@ -638,7 +592,7 @@ paths: - $ref: '#/definitions/utils.RespStatus' - properties: data: - $ref: '#/definitions/service_event.InfoResult' + $ref: '#/definitions/service_event.InfoResponse' type: object "400": description: Invalid Input @@ -686,7 +640,7 @@ paths: - $ref: '#/definitions/utils.RespStatus' - properties: data: - $ref: '#/definitions/service_user.UserTableResult' + $ref: '#/definitions/service_user.UserTableResponse' type: object "500": description: Internal Server Error (Database Error) diff --git a/generate.go b/generate.go index 64e68fb..bae9f07 100644 --- a/generate.go +++ b/generate.go @@ -1,3 +1,5 @@ package main //go:generate go run ./cmd/gen_exception/main.go + +//go:generate swag init diff --git a/justfile b/justfile index 17040f5..79e84cf 100644 --- a/justfile +++ b/justfile @@ -39,7 +39,7 @@ gen-back: cd {{ project_dir }} && go generate . watch-back: - watchexec -r -e go,yaml,tpl -i '.devenv/**' -i '.direnv/**' -i 'client/**' -i 'vendor/**' 'swag init && 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 generate devenv up --verbose diff --git a/service/service_auth/exchange.go b/service/service_auth/exchange.go index b4396ee..5d3070c 100644 --- a/service/service_auth/exchange.go +++ b/service/service_auth/exchange.go @@ -23,11 +23,13 @@ type ExchangePayload struct { Data *ExchangeData } +type ExchangeResponse struct { + RedirectUri string `json:"redirect_uri"` +} + type ExchangeResult struct { Common shared.CommonResult - Data *struct { - RedirectUri string `json:"redirect_uri"` - } + Data *ExchangeResponse } func (self *AuthServiceImpl) Exchange(payload *ExchangePayload) (result *ExchangeResult) { @@ -113,16 +115,14 @@ func (self *AuthServiceImpl) Exchange(payload *ExchangePayload) (result *Exchang SetError(nil). Throw(payload.Context) - resultData := struct { - RedirectUri string `json:"redirect_uri"` - }{url.String()} + resultData := &ExchangeResponse{url.String()} result = &ExchangeResult{ Common: shared.CommonResult{ HttpCode: 200, Exception: exception, }, - Data: &resultData, + Data: resultData, } return diff --git a/service/service_auth/magic.go b/service/service_auth/magic.go index 15a6e2c..e613bba 100644 --- a/service/service_auth/magic.go +++ b/service/service_auth/magic.go @@ -26,9 +26,13 @@ type MagicPayload struct { Data *MagicData } +type MagicResponse struct { + Uri string `json:"uri"` +} + type MagicResult struct { Common shared.CommonResult - Data any + Data *MagicResponse } func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult) { @@ -129,7 +133,7 @@ func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult) HttpCode: 200, Exception: exception, }, - Data: uriData, + Data: &MagicResponse{uriData.Uri}, } return diff --git a/service/service_event/checkin.go b/service/service_event/checkin.go index 7079554..ba89bb1 100644 --- a/service/service_event/checkin.go +++ b/service/service_event/checkin.go @@ -20,11 +20,13 @@ type CheckinPayload struct { Data *CheckinData } +type CheckinResponse struct { + CheckinCode *string `json:"checkin_code"` +} + type CheckinResult struct { Common shared.CommonResult - Data *struct { - CheckinCode *string `json:"checkin_code"` - } + Data *CheckinResponse } func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) { @@ -44,6 +46,7 @@ func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinR Throw(payload.Context), }, } + return } @@ -58,10 +61,9 @@ func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinR SetOriginal(exception.CommonSuccess). Throw(payload.Context), }, - Data: &struct { - CheckinCode *string `json:"checkin_code"` - }{code}, + Data: &CheckinResponse{code}, } + return } @@ -123,11 +125,13 @@ type CheckinQueryPayload struct { Data *CheckinQueryData } +type CheckinQueryResponse struct { + CheckinAt *time.Time `json:"checkin_at"` +} + type CheckinQueryResult struct { Common shared.CommonResult - Data *struct { - CheckinAt *time.Time `json:"checkin_at"` - } + Data *CheckinQueryResponse } func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result *CheckinQueryResult) { @@ -183,9 +187,7 @@ func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result SetOriginal(exception.CommonSuccess). Throw(payload.Context), }, - Data: &struct { - CheckinAt *time.Time `json:"checkin_at"` - }{checkinAt}, + Data: &CheckinQueryResponse{checkinAt}, } return } diff --git a/service/service_event/info.go b/service/service_event/info.go index 743fb21..06c7c1f 100644 --- a/service/service_event/info.go +++ b/service/service_event/info.go @@ -19,13 +19,15 @@ type InfoPayload struct { Data *InfoData } +type InfoResponse struct { + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +} + type InfoResult struct { Common shared.CommonResult - Data *struct { - Name string `json:"name"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - } + Data *InfoResponse } func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) { @@ -50,16 +52,6 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) { return } - resultData := struct { - Name string `json:"name"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - }{ - Name: event.Name, - StartTime: event.StartTime, - EndTime: event.EndTime, - } - result = &InfoResult{ Common: shared.CommonResult{ HttpCode: 200, @@ -71,7 +63,11 @@ func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) { SetOriginal(exception.CommonSuccess). Throw(payload.Context), }, - Data: &resultData, + Data: &InfoResponse{ + Name: event.Name, + StartTime: event.StartTime, + EndTime: event.EndTime, + }, } return diff --git a/service/service_user/list_user_full_table.go b/service/service_user/list_user_full_table.go index f27bd65..31553ef 100644 --- a/service/service_user/list_user_full_table.go +++ b/service/service_user/list_user_full_table.go @@ -11,9 +11,13 @@ type UserTablePayload struct { Context context.Context } +type UserTableResponse struct { + UserTable *[]data.User `json:"user_table"` +} + type UserTableResult struct { Common shared.CommonResult - Data *[]data.User `json:"user_table"` + Data *UserTableResponse } // ListUserFullTable @@ -58,7 +62,7 @@ func (self *UserServiceImpl) GetUserFullTable(payload *UserTablePayload) (result HttpCode: 200, Exception: exception, }, - Data: userFullTable, + Data: &UserTableResponse{userFullTable}, } return -- 2.49.1 From 937f382f933d77e70416e952c81cefcbc918ed9f Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 01:28:19 +0800 Subject: [PATCH 020/195] Fix swagger auth docs Signed-off-by: Asai Neko --- api/auth/exchange.go | 1 - api/event/checkin.go | 3 +++ api/event/info.go | 1 + api/user/list.go | 1 + docs/docs.go | 30 +++++++++++++++++++++++++----- docs/swagger.json | 30 +++++++++++++++++++++++++----- docs/swagger.yaml | 12 ++++++++++-- 7 files changed, 65 insertions(+), 13 deletions(-) diff --git a/api/auth/exchange.go b/api/auth/exchange.go index 4f791e6..c3dc69f 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -20,7 +20,6 @@ import ( // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Security ApiKeyAuth // @Router /auth/exchange [post] func (self *AuthHandler) Exchange(c *gin.Context) { var exchangeData service_auth.ExchangeData diff --git a/api/event/checkin.go b/api/event/checkin.go index be7274b..1eb8767 100644 --- a/api/event/checkin.go +++ b/api/event/checkin.go @@ -19,6 +19,7 @@ import ( // @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Security ApiKeyAuth // @Router /event/checkin [get] func (self *EventHandler) Checkin(c *gin.Context) { userIdOrig, _ := c.Get("user_id") @@ -56,6 +57,7 @@ func (self *EventHandler) Checkin(c *gin.Context) { // @Param payload body service_event.CheckinSubmitData true "Checkin Code Data" // @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input" +// @Security ApiKeyAuth // @Router /event/checkin/submit [post] func (self *EventHandler) CheckinSubmit(c *gin.Context) { var data service_event.CheckinSubmitData @@ -89,6 +91,7 @@ func (self *EventHandler) CheckinSubmit(c *gin.Context) { // @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found" +// @Security ApiKeyAuth // @Router /event/checkin/query [get] func (self *EventHandler) CheckinQuery(c *gin.Context) { userIdOrig, _ := c.Get("user_id") diff --git a/api/event/info.go b/api/event/info.go index ec86f6f..9af5895 100644 --- a/api/event/info.go +++ b/api/event/info.go @@ -20,6 +20,7 @@ import ( // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" // @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Security ApiKeyAuth // @Router /event/info [get] func (self *EventHandler) Info(c *gin.Context) { eventIdOrig := c.Query("event_id") diff --git a/api/user/list.go b/api/user/list.go index 08478bc..9efd91a 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -19,6 +19,7 @@ import ( // @Success 200 {object} utils.RespStatus{data=[]data.UserSearchDoc} "Successful paginated list retrieval" // @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)" // @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)" +// @Security ApiKeyAuth // @Router /user/list [get] func (self *UserHandler) List(c *gin.Context) { type ListQuery struct { diff --git a/docs/docs.go b/docs/docs.go index 39243cc..13e07b6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -17,11 +17,6 @@ const docTemplate = `{ "paths": { "/auth/exchange": { "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Exchanges client credentials and user session for a specific redirect authorization code.", "consumes": [ "application/json" @@ -529,6 +524,11 @@ const docTemplate = `{ }, "/event/checkin": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Creates a temporary check-in code for the authenticated user and event.", "consumes": [ "application/json" @@ -609,6 +609,11 @@ const docTemplate = `{ }, "/event/checkin/query": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Returns the timestamp of when the user checked in, or null if not yet checked in.", "consumes": [ "application/json" @@ -689,6 +694,11 @@ const docTemplate = `{ }, "/event/checkin/submit": { "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Submits the generated code to mark the user as attended.", "consumes": [ "application/json" @@ -753,6 +763,11 @@ const docTemplate = `{ }, "/event/info": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Fetches the name, start time, and end time of an event using its UUID.", "consumes": [ "application/json" @@ -1003,6 +1018,11 @@ const docTemplate = `{ }, "/user/list": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.", "consumes": [ "application/json" diff --git a/docs/swagger.json b/docs/swagger.json index 8873d65..d1426cb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6,11 +6,6 @@ "paths": { "/auth/exchange": { "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Exchanges client credentials and user session for a specific redirect authorization code.", "consumes": [ "application/json" @@ -518,6 +513,11 @@ }, "/event/checkin": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Creates a temporary check-in code for the authenticated user and event.", "consumes": [ "application/json" @@ -598,6 +598,11 @@ }, "/event/checkin/query": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Returns the timestamp of when the user checked in, or null if not yet checked in.", "consumes": [ "application/json" @@ -678,6 +683,11 @@ }, "/event/checkin/submit": { "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Submits the generated code to mark the user as attended.", "consumes": [ "application/json" @@ -742,6 +752,11 @@ }, "/event/info": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Fetches the name, start time, and end time of an event using its UUID.", "consumes": [ "application/json" @@ -992,6 +1007,11 @@ }, "/user/list": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.", "consumes": [ "application/json" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e32d97f..72821b7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -209,8 +209,6 @@ paths: data: type: object type: object - security: - - ApiKeyAuth: [] summary: Exchange Auth Code tags: - Authentication @@ -486,6 +484,8 @@ paths: data: type: object type: object + security: + - ApiKeyAuth: [] summary: Generate Check-in Code tags: - Event @@ -531,6 +531,8 @@ paths: data: type: object type: object + security: + - ApiKeyAuth: [] summary: Query Check-in Status tags: - Event @@ -567,6 +569,8 @@ paths: data: type: object type: object + security: + - ApiKeyAuth: [] summary: Submit Check-in Code tags: - Event @@ -621,6 +625,8 @@ paths: data: type: object type: object + security: + - ApiKeyAuth: [] summary: Get Event Information tags: - Event @@ -754,6 +760,8 @@ paths: data: type: object type: object + security: + - ApiKeyAuth: [] summary: List Users tags: - User -- 2.49.1 From c75423bf84ca083fbc1ef4089da6b112bed6c3f8 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 01:31:23 +0800 Subject: [PATCH 021/195] Fix backend Containerfile Signed-off-by: Asai Neko --- container/backend.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/container/backend.Containerfile b/container/backend.Containerfile index b20ef03..10cec6b 100644 --- a/container/backend.Containerfile +++ b/container/backend.Containerfile @@ -2,6 +2,7 @@ FROM docker.io/golang:1.25.5-alpine AS backend-build WORKDIR /app COPY . /app RUN go mod tidy && \ + go generate . \ go build -o /app/nixcn-cms FROM docker.io/alpine:3.23 -- 2.49.1 From 44a97c6d0f4edeeaff3fb475cc6db0ff4b6c965a Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 01:32:49 +0800 Subject: [PATCH 022/195] Fix backend Containerfile Signed-off-by: Asai Neko --- container/backend.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/container/backend.Containerfile b/container/backend.Containerfile index 10cec6b..ff7f829 100644 --- a/container/backend.Containerfile +++ b/container/backend.Containerfile @@ -1,6 +1,7 @@ FROM docker.io/golang:1.25.5-alpine AS backend-build WORKDIR /app COPY . /app +RUN go install github.com/swaggo/swag/cmd/swag@latest RUN go mod tidy && \ go generate . \ go build -o /app/nixcn-cms -- 2.49.1 From 3ac1f4165f9d48071620d5cb26586f2bda9f25d3 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 01:35:56 +0800 Subject: [PATCH 023/195] Fix backend Containerfile Signed-off-by: Asai Neko --- container/backend.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container/backend.Containerfile b/container/backend.Containerfile index ff7f829..67984db 100644 --- a/container/backend.Containerfile +++ b/container/backend.Containerfile @@ -3,7 +3,7 @@ WORKDIR /app COPY . /app RUN go install github.com/swaggo/swag/cmd/swag@latest RUN go mod tidy && \ - go generate . \ + go generate . && \ go build -o /app/nixcn-cms FROM docker.io/alpine:3.23 -- 2.49.1 From 2a0788ea8675c994854f55fbaf3eba38f1bb0fb3 Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Thu, 29 Jan 2026 09:39:09 +0800 Subject: [PATCH 024/195] fix(client): no grayscale on avatar Signed-off-by: Noa Virellia --- client/cms/src/components/sidebar/nav-user.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cms/src/components/sidebar/nav-user.tsx b/client/cms/src/components/sidebar/nav-user.tsx index 8356344..0917530 100644 --- a/client/cms/src/components/sidebar/nav-user.tsx +++ b/client/cms/src/components/sidebar/nav-user.tsx @@ -41,7 +41,7 @@ function NavUser_() { size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > - + CN -- 2.49.1 From 220b4d2ea3fc08561ad8b6f833513ac46f6dd144 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 10:19:34 +0800 Subject: [PATCH 025/195] Optimize swagger Signed-off-by: Asai Neko --- api/auth/exchange.go | 22 +++++++-------- api/auth/magic.go | 22 +++++++-------- api/auth/redirect.go | 30 ++++++++++----------- api/auth/refresh.go | 22 +++++++-------- api/auth/token.go | 22 +++++++-------- api/event/checkin.go | 64 ++++++++++++++++++++++---------------------- api/event/info.go | 24 ++++++++--------- api/user/full.go | 18 ++++++------- api/user/info.go | 22 +++++++-------- api/user/list.go | 24 ++++++++--------- api/user/update.go | 26 +++++++++--------- docs/docs.go | 23 +++++++++++----- docs/swagger.json | 20 +++++++++++++- docs/swagger.yaml | 17 +++++++++++- generate.go | 3 ++- go.mod | 2 +- server/server.go | 5 ++-- 17 files changed, 205 insertions(+), 161 deletions(-) diff --git a/api/auth/exchange.go b/api/auth/exchange.go index c3dc69f..a128108 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -10,17 +10,17 @@ import ( ) // Exchange handles the authorization code swap process. -// @Summary Exchange Auth Code -// @Description Exchanges client credentials and user session for a specific redirect authorization code. -// @Tags Authentication -// @Accept json -// @Produce json -// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials" -// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResponse} "Successful exchange" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Router /auth/exchange [post] +// @Summary Exchange Auth Code +// @Description Exchanges client credentials and user session for a specific redirect authorization code. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials" +// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResponse} "Successful exchange" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Router /auth/exchange [post] func (self *AuthHandler) Exchange(c *gin.Context) { var exchangeData service_auth.ExchangeData diff --git a/api/auth/magic.go b/api/auth/magic.go index d54a81a..31469c9 100644 --- a/api/auth/magic.go +++ b/api/auth/magic.go @@ -9,17 +9,17 @@ import ( ) // Magic handles the "Magic Link" authentication request. -// @Summary Request Magic Link -// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. -// @Tags Authentication -// @Accept json -// @Produce json -// @Param payload body service_auth.MagicData true "Magic Link Request Data" -// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResponse} "Successful request" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Router /auth/magic [post] +// @Summary Request Magic Link +// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.MagicData true "Magic Link Request Data" +// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResponse} "Successful request" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Router /auth/magic [post] func (self *AuthHandler) Magic(c *gin.Context) { var magicData service_auth.MagicData diff --git a/api/auth/redirect.go b/api/auth/redirect.go index 28cfc8e..aa56dda 100644 --- a/api/auth/redirect.go +++ b/api/auth/redirect.go @@ -9,21 +9,21 @@ import ( ) // Redirect handles the post-verification callback and redirects the user to the target application. -// @Summary Handle Auth Callback and Redirect -// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. -// @Tags Authentication -// @Accept x-www-form-urlencoded -// @Produce json -// @Produce html -// @Param client_id query string true "Client Identifier" -// @Param redirect_uri query string true "Target Redirect URI" -// @Param code query string true "Temporary Verification Code" -// @Param state query string false "Opaque state used to maintain state between the request and callback" -// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Client Not Found / URI Mismatch" -// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Verification Code" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Router /auth/redirect [get] +// @Summary Handle Auth Callback and Redirect +// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. +// @Tags Authentication +// @Accept x-www-form-urlencoded +// @Produce json +// @Produce html +// @Param client_id query string true "Client Identifier" +// @Param redirect_uri query string true "Target Redirect URI" +// @Param code query string true "Temporary Verification Code" +// @Param state query string false "Opaque state used to maintain state between the request and callback" +// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Client Not Found / URI Mismatch" +// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Verification Code" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Router /auth/redirect [get] func (self *AuthHandler) Redirect(c *gin.Context) { data := &service_auth.RedirectData{ ClientId: c.Query("client_id"), diff --git a/api/auth/refresh.go b/api/auth/refresh.go index 850e9f0..8205108 100644 --- a/api/auth/refresh.go +++ b/api/auth/refresh.go @@ -9,17 +9,17 @@ import ( ) // Refresh handles the token rotation process. -// @Summary Refresh Access Token -// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token. -// @Tags Authentication -// @Accept json -// @Produce json -// @Param payload body service_auth.RefreshData true "Refresh Token Body" -// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful rotation" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 401 {object} utils.RespStatus{data=nil} "Invalid Refresh Token" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Router /auth/refresh [post] +// @Summary Refresh Access Token +// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.RefreshData true "Refresh Token Body" +// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful rotation" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 401 {object} utils.RespStatus{data=nil} "Invalid Refresh Token" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Router /auth/refresh [post] func (self *AuthHandler) Refresh(c *gin.Context) { var refreshData service_auth.RefreshData diff --git a/api/auth/token.go b/api/auth/token.go index d841766..c8257bb 100644 --- a/api/auth/token.go +++ b/api/auth/token.go @@ -9,17 +9,17 @@ import ( ) // Token exchanges an authorization code for access and refresh tokens. -// @Summary Exchange Code for Token -// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). -// @Tags Authentication -// @Accept json -// @Produce json -// @Param payload body service_auth.TokenData true "Token Request Body" -// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful token issuance" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Code" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Router /auth/token [post] +// @Summary Exchange Code for Token +// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.TokenData true "Token Request Body" +// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful token issuance" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Code" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Router /auth/token [post] func (self *AuthHandler) Token(c *gin.Context) { var tokenData service_auth.TokenData diff --git a/api/event/checkin.go b/api/event/checkin.go index 1eb8767..3c25084 100644 --- a/api/event/checkin.go +++ b/api/event/checkin.go @@ -10,17 +10,17 @@ import ( ) // Checkin generates a check-in code for a specific event. -// @Summary Generate Check-in Code -// @Description Creates a temporary check-in code for the authenticated user and event. -// @Tags Event -// @Accept json -// @Produce json -// @Param event_id query string true "Event UUID" -// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Security ApiKeyAuth -// @Router /event/checkin [get] +// @Summary Generate Check-in Code +// @Description Creates a temporary check-in code for the authenticated user and event. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Security ApiKeyAuth +// @Router /event/checkin [get] func (self *EventHandler) Checkin(c *gin.Context) { userIdOrig, _ := c.Get("user_id") userId, _ := uuid.Parse(userIdOrig.(string)) @@ -49,16 +49,16 @@ func (self *EventHandler) Checkin(c *gin.Context) { } // CheckinSubmit validates a check-in code to complete attendance. -// @Summary Submit Check-in Code -// @Description Submits the generated code to mark the user as attended. -// @Tags Event -// @Accept json -// @Produce json -// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data" -// @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input" -// @Security ApiKeyAuth -// @Router /event/checkin/submit [post] +// @Summary Submit Check-in Code +// @Description Submits the generated code to mark the user as attended. +// @Tags Event +// @Accept json +// @Produce json +// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data" +// @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input" +// @Security ApiKeyAuth +// @Router /event/checkin/submit [post] func (self *EventHandler) CheckinSubmit(c *gin.Context) { var data service_event.CheckinSubmitData if err := c.ShouldBindJSON(&data); err != nil { @@ -82,17 +82,17 @@ func (self *EventHandler) CheckinSubmit(c *gin.Context) { } // CheckinQuery retrieves the check-in status of a user for an event. -// @Summary Query Check-in Status -// @Description Returns the timestamp of when the user checked in, or null if not yet checked in. -// @Tags Event -// @Accept json -// @Produce json -// @Param event_id query string true "Event UUID" -// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found" -// @Security ApiKeyAuth -// @Router /event/checkin/query [get] +// @Summary Query Check-in Status +// @Description Returns the timestamp of when the user checked in, or null if not yet checked in. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found" +// @Security ApiKeyAuth +// @Router /event/checkin/query [get] func (self *EventHandler) CheckinQuery(c *gin.Context) { userIdOrig, _ := c.Get("user_id") userId, _ := uuid.Parse(userIdOrig.(string)) diff --git a/api/event/info.go b/api/event/info.go index 9af5895..232059c 100644 --- a/api/event/info.go +++ b/api/event/info.go @@ -10,18 +10,18 @@ import ( ) // Info retrieves basic information about a specific event. -// @Summary Get Event Information -// @Description Fetches the name, start time, and end time of an event using its UUID. -// @Tags Event -// @Accept json -// @Produce json -// @Param event_id query string true "Event UUID" -// @Success 200 {object} utils.RespStatus{data=service_event.InfoResponse} "Successful retrieval" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" -// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" -// @Security ApiKeyAuth -// @Router /event/info [get] +// @Summary Get Event Information +// @Description Fetches the name, start time, and end time of an event using its UUID. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} utils.RespStatus{data=service_event.InfoResponse} "Successful retrieval" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input" +// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Security ApiKeyAuth +// @Router /event/info [get] func (self *EventHandler) Info(c *gin.Context) { eventIdOrig := c.Query("event_id") eventId, err := uuid.Parse(eventIdOrig) diff --git a/api/user/full.go b/api/user/full.go index 35bf3d9..a94b6d7 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -9,15 +9,15 @@ import ( ) // Full retrieves the complete list of users directly from the database table. -// @Summary Get Full User Table -// @Description Fetches all user records without pagination. This is typically used for administrative overview or data export. -// @Tags User -// @Accept json -// @Produce json -// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResponse} "Successful retrieval of full user table" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error)" -// @Security ApiKeyAuth -// @Router /user/full [get] +// @Summary Get Full User Table +// @Description Fetches all user records without pagination. This is typically used for administrative overview or data export. +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResponse} "Successful retrieval of full user table" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error)" +// @Security ApiKeyAuth +// @Router /user/full [get] func (self *UserHandler) Full(c *gin.Context) { userTablePayload := &service_user.UserTablePayload{ Context: c, diff --git a/api/user/info.go b/api/user/info.go index faeeac1..70b42e0 100644 --- a/api/user/info.go +++ b/api/user/info.go @@ -10,17 +10,17 @@ import ( ) // Info retrieves the profile information of the currently authenticated user. -// @Summary Get My User Information -// @Description Fetches the complete profile data for the user associated with the provided session/token. -// @Tags User -// @Accept json -// @Produce json -// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval" -// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized" -// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)" -// @Security ApiKeyAuth -// @Router /user/info [get] +// @Summary Get My User Information +// @Description Fetches the complete profile data for the user associated with the provided session/token. +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval" +// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized" +// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)" +// @Security ApiKeyAuth +// @Router /user/info [get] func (self *UserHandler) Info(c *gin.Context) { userIdOrig, ok := c.Get("user_id") if !ok { diff --git a/api/user/list.go b/api/user/list.go index 9efd91a..bc3dc7b 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -9,18 +9,18 @@ import ( ) // List retrieves a paginated list of users from the search engine. -// @Summary List Users -// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. -// @Tags User -// @Accept json -// @Produce json -// @Param limit query string false "Maximum number of users to return (default 0)" -// @Param offset query string true "Number of users to skip" -// @Success 200 {object} utils.RespStatus{data=[]data.UserSearchDoc} "Successful paginated list retrieval" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)" -// @Security ApiKeyAuth -// @Router /user/list [get] +// @Summary List Users +// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. +// @Tags User +// @Accept json +// @Produce json +// @Param limit query string false "Maximum number of users to return (default 0)" +// @Param offset query string true "Number of users to skip" +// @Success 200 {object} utils.RespStatus{data=[]data.UserSearchDoc} "Successful paginated list retrieval" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)" +// @Security ApiKeyAuth +// @Router /user/list [get] func (self *UserHandler) List(c *gin.Context) { type ListQuery struct { Limit *string `form:"limit"` diff --git a/api/user/update.go b/api/user/update.go index e525c9d..68989f4 100644 --- a/api/user/update.go +++ b/api/user/update.go @@ -10,19 +10,19 @@ import ( ) // Update modifies the profile information for the currently authenticated user. -// @Summary Update User Information -// @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). -// @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). -// @Tags User -// @Accept json -// @Produce json -// @Param payload body service_user.UserInfoData true "Updated User Profile Data" -// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update" -// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)" -// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized" -// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)" -// @Security ApiKeyAuth -// @Router /user/update [patch] +// @Summary Update User Information +// @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). +// @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). +// @Tags User +// @Accept json +// @Produce json +// @Param payload body service_user.UserInfoData true "Updated User Profile Data" +// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)" +// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)" +// @Security ApiKeyAuth +// @Router /user/update [patch] func (self *UserHandler) Update(c *gin.Context) { userIdOrig, ok := c.Get("user_id") if !ok { diff --git a/docs/docs.go b/docs/docs.go index 13e07b6..b72cea5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -9,7 +9,16 @@ const docTemplate = `{ "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", - "contact": {}, + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, "version": "{{.Version}}" }, "host": "{{.Host}}", @@ -1462,12 +1471,12 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "", - Host: "", - BasePath: "", - Schemes: []string{}, - Title: "", - Description: "", + Version: "1.0", + Host: "localhost:8000", + BasePath: "/api/v1", + Schemes: []string{"http", "https"}, + Title: "NixCN CMS API", + Description: "API Docs based on Gin framework", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index d1426cb..89415d4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,8 +1,26 @@ { + "schemes": [ + "http", + "https" + ], "swagger": "2.0", "info": { - "contact": {} + "description": "API Docs based on Gin framework", + "title": "NixCN CMS API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" }, + "host": "localhost:8000", + "basePath": "/api/v1", "paths": { "/auth/exchange": { "post": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 72821b7..fe26179 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,3 +1,4 @@ +basePath: /api/v1 definitions: data.User: properties: @@ -154,8 +155,19 @@ definitions: status: type: string type: object +host: localhost:8000 info: - contact: {} + contact: + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: API Docs based on Gin framework + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: NixCN CMS API + version: "1.0" paths: /auth/exchange: post: @@ -823,4 +835,7 @@ paths: summary: Update User Information tags: - User +schemes: +- http +- https swagger: "2.0" diff --git a/generate.go b/generate.go index bae9f07..561b1bd 100644 --- a/generate.go +++ b/generate.go @@ -2,4 +2,5 @@ package main //go:generate go run ./cmd/gen_exception/main.go -//go:generate swag init +//go:generate swag fmt +//go:generate swag init -g server/server.go diff --git a/go.mod b/go.mod index e98cf52..3aa5302 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,6 @@ require ( github.com/spf13/viper v1.21.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 - github.com/swaggo/swag v1.16.6 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 @@ -113,6 +112,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/swag v1.16.6 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect diff --git a/server/server.go b/server/server.go index 6aa36f8..bbb899f 100644 --- a/server/server.go +++ b/server/server.go @@ -27,7 +27,7 @@ import ( // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host localhost:8080 +// @host localhost:8000 // @BasePath /api/v1 // @schemes http https func Start(ctx context.Context) { @@ -39,10 +39,11 @@ func Start(ctx context.Context) { r := gin.New() r.Use(otelgin.Middleware(viper.GetString("server.service_name"))) r.Use(middleware.GinLogger()) - r.Use(gin.Recovery()) r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.Use(gin.Recovery()) + api.Handler(r.Group("/api/v1")) // Start http server -- 2.49.1 From f898243de5f7c608079de6a6b2bcdfafba36ffe8 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 10:24:09 +0800 Subject: [PATCH 026/195] Only enable swagger under debug mode Signed-off-by: Asai Neko --- server/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index bbb899f..61a1285 100644 --- a/server/server.go +++ b/server/server.go @@ -40,7 +40,9 @@ func Start(ctx context.Context) { r.Use(otelgin.Middleware(viper.GetString("server.service_name"))) r.Use(middleware.GinLogger()) - r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + if viper.GetBool("server.debug_mode") { + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + } r.Use(gin.Recovery()) -- 2.49.1 From a0f6087d3e48cb1670a8c0a856ab6776fc61f4b8 Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Thu, 29 Jan 2026 11:43:46 +0800 Subject: [PATCH 027/195] refactor(client): use generated API client and hooks Signed-off-by: Noa Virellia --- client/cms/openapi-ts.config.ts | 19 + client/cms/package.json | 4 +- client/cms/pnpm-lock.yaml | 327 +++++++- .../src/client/@tanstack/react-query.gen.ts | 353 +++++++++ client/cms/src/client/client.gen.ts | 16 + client/cms/src/client/client/client.gen.ts | 311 ++++++++ client/cms/src/client/client/index.ts | 25 + client/cms/src/client/client/types.gen.ts | 241 ++++++ client/cms/src/client/client/utils.gen.ts | 332 ++++++++ client/cms/src/client/core/auth.gen.ts | 42 ++ .../cms/src/client/core/bodySerializer.gen.ts | 100 +++ client/cms/src/client/core/params.gen.ts | 176 +++++ .../cms/src/client/core/pathSerializer.gen.ts | 181 +++++ .../src/client/core/queryKeySerializer.gen.ts | 136 ++++ .../src/client/core/serverSentEvents.gen.ts | 266 +++++++ client/cms/src/client/core/types.gen.ts | 118 +++ client/cms/src/client/core/utils.gen.ts | 143 ++++ client/cms/src/client/index.ts | 4 + client/cms/src/client/sdk.gen.ts | 153 ++++ client/cms/src/client/types.gen.ts | 713 ++++++++++++++++++ client/cms/src/client/zod.gen.ts | 280 +++++++ .../cms/src/components/checkin/qr-dialog.tsx | 28 +- client/cms/src/components/login-form.tsx | 2 +- .../profile/edit-profile-dialog.tsx | 5 +- .../src/components/profile/main-profile.tsx | 7 +- .../cms/src/components/sidebar/nav-user.tsx | 3 +- client/cms/src/hooks/data/useExchangeToken.ts | 16 + .../cms/src/hooks/data/useGetCheckInCode.ts | 18 - client/cms/src/hooks/data/useGetMagicLink.ts | 14 +- client/cms/src/hooks/data/useUpdateUser.ts | 18 +- client/cms/src/hooks/data/useUserInfo.ts | 20 +- .../src/hooks/data/useValidateMagicLink.ts | 11 - client/cms/src/lib/axios.ts | 67 -- client/cms/src/lib/client.ts | 63 ++ client/cms/src/lib/token.ts | 21 +- client/cms/src/main.tsx | 3 + client/cms/src/routes/authorize.tsx | 28 +- client/cms/src/routes/token.tsx | 29 +- 38 files changed, 4076 insertions(+), 217 deletions(-) create mode 100644 client/cms/openapi-ts.config.ts create mode 100644 client/cms/src/client/@tanstack/react-query.gen.ts create mode 100644 client/cms/src/client/client.gen.ts create mode 100644 client/cms/src/client/client/client.gen.ts create mode 100644 client/cms/src/client/client/index.ts create mode 100644 client/cms/src/client/client/types.gen.ts create mode 100644 client/cms/src/client/client/utils.gen.ts create mode 100644 client/cms/src/client/core/auth.gen.ts create mode 100644 client/cms/src/client/core/bodySerializer.gen.ts create mode 100644 client/cms/src/client/core/params.gen.ts create mode 100644 client/cms/src/client/core/pathSerializer.gen.ts create mode 100644 client/cms/src/client/core/queryKeySerializer.gen.ts create mode 100644 client/cms/src/client/core/serverSentEvents.gen.ts create mode 100644 client/cms/src/client/core/types.gen.ts create mode 100644 client/cms/src/client/core/utils.gen.ts create mode 100644 client/cms/src/client/index.ts create mode 100644 client/cms/src/client/sdk.gen.ts create mode 100644 client/cms/src/client/types.gen.ts create mode 100644 client/cms/src/client/zod.gen.ts create mode 100644 client/cms/src/hooks/data/useExchangeToken.ts delete mode 100644 client/cms/src/hooks/data/useGetCheckInCode.ts delete mode 100644 client/cms/src/hooks/data/useValidateMagicLink.ts delete mode 100644 client/cms/src/lib/axios.ts create mode 100644 client/cms/src/lib/client.ts diff --git a/client/cms/openapi-ts.config.ts b/client/cms/openapi-ts.config.ts new file mode 100644 index 0000000..c0d6eef --- /dev/null +++ b/client/cms/openapi-ts.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@hey-api/openapi-ts'; + +export default defineConfig({ + input: 'http://10.0.0.10:8000/swagger/doc.json', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + '@hey-api/typescript', + '@tanstack/react-query', + 'zod', + { + name: '@hey-api/transformers', + dates: true, + }, + { + name: '@hey-api/sdk', + transformer: true, + }, + ], +}); diff --git a/client/cms/package.json b/client/cms/package.json index 266c7ff..b01e804 100644 --- a/client/cms/package.json +++ b/client/cms/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "gen": "openapi-ts" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -66,6 +67,7 @@ "@antfu/eslint-config": "^6.7.1", "@eslint-react/eslint-plugin": "^2.3.13", "@eslint/js": "^9.39.1", + "@hey-api/openapi-ts": "0.91.0", "@tailwindcss/typography": "^0.5.19", "@tanstack/eslint-plugin-query": "^5.91.2", "@tanstack/router-plugin": "^1.141.7", diff --git a/client/cms/pnpm-lock.yaml b/client/cms/pnpm-lock.yaml index 7bbc249..1f4dcea 100644 --- a/client/cms/pnpm-lock.yaml +++ b/client/cms/pnpm-lock.yaml @@ -171,6 +171,9 @@ importers: '@eslint/js': specifier: ^9.39.1 version: 9.39.2 + '@hey-api/openapi-ts': + specifier: 0.91.0 + version: 0.91.0(typescript@5.9.3) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.18) @@ -712,6 +715,34 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@hey-api/codegen-core@0.6.0': + resolution: {integrity: sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw==} + engines: {node: '>=20.19.0'} + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/json-schema-ref-parser@1.2.3': + resolution: {integrity: sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ==} + engines: {node: '>= 16'} + + '@hey-api/openapi-ts@0.91.0': + resolution: {integrity: sha512-AHkd982HsPz1XpqRm59URwJyJqTzyzzC30EAp07b/0M9KojjneCPxm8FnvFnXLRTMyKgcOymMsYXuLzJ9mpMHA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/shared@0.1.0': + resolution: {integrity: sha512-qEDMSBWEEWxcBU5XHacjCCnFOVq1YWPPR3owURVep60I7ejfSG5OINxM4eF+p3KJGMcZduzzfq9pd1grStHZBg==} + engines: {node: '>=20.19.0'} + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/types@0.1.3': + resolution: {integrity: sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw==} + peerDependencies: + typescript: '>=5.5.3' + '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -749,6 +780,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@marsidev/react-turnstile@1.4.1': resolution: {integrity: sha512-1jE0IjvB8z+q1NFRs3149gXzXwIzXQWqQjn9fmAr13BiE3RYLWck5Me6flHYE90shW5L12Jkm6R1peS1OnA9oQ==} peerDependencies: @@ -1266,79 +1300,66 @@ packages: resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.2': resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.2': resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.2': resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.2': resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.2': resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.2': resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.2': resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.2': resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.2': resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.2': resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.2': resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.2': resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.55.2': resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==} @@ -1497,28 +1518,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -1924,6 +1941,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@7.2.0: resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} @@ -2027,6 +2048,18 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2076,10 +2109,20 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2109,6 +2152,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2143,6 +2190,10 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2246,6 +2297,21 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2254,6 +2320,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2285,6 +2354,10 @@ packages: dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2737,6 +2810,10 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -2910,6 +2987,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2935,6 +3017,15 @@ packages: eslint: '*' typescript: '>=4.7.4' + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2943,6 +3034,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isbot@5.1.33: resolution: {integrity: sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg==} engines: {node: '>=18'} @@ -3037,28 +3132,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -3330,6 +3421,9 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -3340,6 +3434,11 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nypm@0.6.4: + resolution: {integrity: sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3347,10 +3446,17 @@ packages: object-deep-merge@2.0.0: resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3420,6 +3526,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3465,6 +3574,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3498,6 +3611,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -3581,6 +3697,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -3696,6 +3816,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4093,6 +4217,10 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -4635,6 +4763,53 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@hey-api/codegen-core@0.6.0(typescript@5.9.3)': + dependencies: + '@hey-api/types': 0.1.3(typescript@5.9.3) + ansi-colors: 4.1.3 + c12: 3.3.3 + color-support: 1.1.3 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/json-schema-ref-parser@1.2.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + lodash: 4.17.21 + + '@hey-api/openapi-ts@0.91.0(typescript@5.9.3)': + dependencies: + '@hey-api/codegen-core': 0.6.0(typescript@5.9.3) + '@hey-api/json-schema-ref-parser': 1.2.3 + '@hey-api/shared': 0.1.0(typescript@5.9.3) + '@hey-api/types': 0.1.3(typescript@5.9.3) + ansi-colors: 4.1.3 + color-support: 1.1.3 + commander: 14.0.2 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/shared@0.1.0(typescript@5.9.3)': + dependencies: + '@hey-api/codegen-core': 0.6.0(typescript@5.9.3) + '@hey-api/json-schema-ref-parser': 1.2.3 + '@hey-api/types': 0.1.3(typescript@5.9.3) + ansi-colors: 4.1.3 + cross-spawn: 7.0.6 + open: 11.0.0 + semver: 7.7.3 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/types@0.1.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.3))': dependencies: '@standard-schema/utils': 0.3.0 @@ -4670,6 +4845,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsdevtools/ono@7.1.3': {} + '@marsidev/react-turnstile@1.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: react: 19.2.3 @@ -5830,6 +6007,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-colors@4.1.3: {} + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 @@ -5928,6 +6107,25 @@ snapshots: builtin-modules@5.0.0: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.3: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.3 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -5972,8 +6170,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + ci-info@4.3.1: {} + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -6005,6 +6213,8 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + colorette@2.0.20: {} combined-stream@1.0.8: @@ -6027,6 +6237,8 @@ snapshots: confbox@0.2.2: {} + consola@3.4.2: {} + convert-source-map@2.0.0: {} cookie-es@2.0.0: {} @@ -6110,10 +6322,23 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.4: {} + delayed-stream@1.0.0: {} dequal@2.0.3: {} + destr@2.0.5: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -6140,6 +6365,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6721,6 +6948,15 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.4 + pathe: 2.0.3 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -6955,6 +7191,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6979,10 +7217,20 @@ snapshots: transitivePeerDependencies: - supports-color + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-number@7.0.0: {} is-plain-obj@4.1.0: {} + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isbot@5.1.33: {} isexe@2.0.0: {} @@ -7561,6 +7809,8 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-fetch-native@1.6.7: {} + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -7569,14 +7819,31 @@ snapshots: dependencies: boolbase: 1.0.0 + nypm@0.6.4: + dependencies: + citty: 0.2.0 + pathe: 2.0.3 + tinyexec: 1.0.2 + object-assign@4.1.1: {} object-deep-merge@2.0.0: {} + ohash@2.0.11: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 + open@11.0.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7649,6 +7916,8 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7693,6 +7962,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + prelude-ls@1.2.1: {} prettier@3.8.0: {} @@ -7719,6 +7990,11 @@ snapshots: quansync@0.2.11: {} + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -7819,6 +8095,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@5.0.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -8029,6 +8307,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.55.2 fsevents: 2.3.3 + run-applescript@7.1.0: {} + scheduler@0.27.0: {} scslre@0.3.0: @@ -8415,6 +8695,11 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + xml-name-validator@4.0.0: {} y18n@4.0.3: {} diff --git a/client/cms/src/client/@tanstack/react-query.gen.ts b/client/cms/src/client/@tanstack/react-query.gen.ts new file mode 100644 index 0000000..6c77868 --- /dev/null +++ b/client/cms/src/client/@tanstack/react-query.gen.ts @@ -0,0 +1,353 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; + +import { client } from '../client.gen'; +import { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getUserFull, getUserInfo, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from '../sdk.gen'; +import type { GetAuthRedirectData, GetAuthRedirectError, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetUserFullData, GetUserFullError, GetUserFullResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse } from '../types.gen'; + +/** + * Exchange Auth Code + * + * Exchanges client credentials and user session for a specific redirect authorization code. + */ +export const postAuthExchangeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthExchange({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Request Magic Link + * + * Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. + */ +export const postAuthMagicMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthMagic({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + } +]; + +const createQueryKey = (id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray): [ + QueryKey[0] +] => { + const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + +export const getAuthRedirectQueryKey = (options: Options) => createQueryKey('getAuthRedirect', options); + +/** + * Handle Auth Callback and Redirect + * + * Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. + */ +export const getAuthRedirectOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getAuthRedirect({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getAuthRedirectQueryKey(options) +}); + +/** + * Refresh Access Token + * + * Accepts a valid refresh token to issue a new access token and a rotated refresh token. + */ +export const postAuthRefreshMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthRefresh({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Exchange Code for Token + * + * Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). + */ +export const postAuthTokenMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postAuthToken({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getEventCheckinQueryKey = (options: Options) => createQueryKey('getEventCheckin', options); + +/** + * Generate Check-in Code + * + * Creates a temporary check-in code for the authenticated user and event. + */ +export const getEventCheckinOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEventCheckin({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getEventCheckinQueryKey(options) +}); + +export const getEventCheckinQueryQueryKey = (options: Options) => createQueryKey('getEventCheckinQuery', options); + +/** + * Query Check-in Status + * + * Returns the timestamp of when the user checked in, or null if not yet checked in. + */ +export const getEventCheckinQueryOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEventCheckinQuery({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getEventCheckinQueryQueryKey(options) +}); + +/** + * Submit Check-in Code + * + * Submits the generated code to mark the user as attended. + */ +export const postEventCheckinSubmitMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postEventCheckinSubmit({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getEventInfoQueryKey = (options: Options) => createQueryKey('getEventInfo', options); + +/** + * Get Event Information + * + * Fetches the name, start time, and end time of an event using its UUID. + */ +export const getEventInfoOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEventInfo({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getEventInfoQueryKey(options) +}); + +export const getUserFullQueryKey = (options?: Options) => createQueryKey('getUserFull', options); + +/** + * Get Full User Table + * + * Fetches all user records without pagination. This is typically used for administrative overview or data export. + */ +export const getUserFullOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserFull({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserFullQueryKey(options) +}); + +export const getUserInfoQueryKey = (options?: Options) => createQueryKey('getUserInfo', options); + +/** + * Get My User Information + * + * Fetches the complete profile data for the user associated with the provided session/token. + */ +export const getUserInfoOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserInfo({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserInfoQueryKey(options) +}); + +export const getUserListQueryKey = (options: Options) => createQueryKey('getUserList', options); + +/** + * List Users + * + * Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. + */ +export const getUserListOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserList({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserListQueryKey(options) +}); + +const createInfiniteParams = [0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey, page: K) => { + const params = { ...queryKey[0] }; + if (page.body) { + params.body = { + ...queryKey[0].body as any, + ...page.body as any + }; + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers + }; + } + if (page.path) { + params.path = { + ...queryKey[0].path as any, + ...page.path as any + }; + } + if (page.query) { + params.query = { + ...queryKey[0].query as any, + ...page.query as any + }; + } + return params as unknown as typeof page; +}; + +export const getUserListInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('getUserList', options, true); + +/** + * List Users + * + * Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. + */ +export const getUserListInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +// @ts-ignore +{ + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { + query: { + offset: pageParam + } + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await getUserList({ + ...options, + ...params, + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserListInfiniteQueryKey(options) +}); + +/** + * Update User Information + * + * Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). + * Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). + */ +export const patchUserUpdateMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await patchUserUpdate({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; diff --git a/client/cms/src/client/client.gen.ts b/client/cms/src/client/client.gen.ts new file mode 100644 index 0000000..36ba69f --- /dev/null +++ b/client/cms/src/client/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig({ baseUrl: 'http://localhost:8000/api/v1' })); diff --git a/client/cms/src/client/client/client.gen.ts b/client/cms/src/client/client/client.gen.ts new file mode 100644 index 0000000..d4cbcce --- /dev/null +++ b/client/cms/src/client/client/client.gen.ts @@ -0,0 +1,311 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response: Response; + + try { + response = await _fetch(request); + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn( + error, + undefined as any, + request, + opts, + )) as unknown; + } + } + + finalError = finalError || ({} as unknown); + + if (opts.throwOnError) { + throw finalError; + } + + // Return error response + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response: undefined as any, + }; + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as + | BodyInit + | null + | undefined, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/client/cms/src/client/client/index.ts b/client/cms/src/client/client/index.ts new file mode 100644 index 0000000..b295ede --- /dev/null +++ b/client/cms/src/client/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/client/cms/src/client/client/types.gen.ts b/client/cms/src/client/client/types.gen.ts new file mode 100644 index 0000000..b4a499c --- /dev/null +++ b/client/cms/src/client/client/types.gen.ts @@ -0,0 +1,241 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/client/cms/src/client/client/utils.gen.ts b/client/cms/src/client/client/utils.gen.ts new file mode 100644 index 0000000..4c48a9e --- /dev/null +++ b/client/cms/src/client/client/utils.gen.ts @@ -0,0 +1,332 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/client/cms/src/client/core/auth.gen.ts b/client/cms/src/client/core/auth.gen.ts new file mode 100644 index 0000000..f8a7326 --- /dev/null +++ b/client/cms/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/client/cms/src/client/core/bodySerializer.gen.ts b/client/cms/src/client/core/bodySerializer.gen.ts new file mode 100644 index 0000000..552b50f --- /dev/null +++ b/client/cms/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,100 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/client/cms/src/client/core/params.gen.ts b/client/cms/src/client/core/params.gen.ts new file mode 100644 index 0000000..602715c --- /dev/null +++ b/client/cms/src/client/core/params.gen.ts @@ -0,0 +1,176 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/client/cms/src/client/core/pathSerializer.gen.ts b/client/cms/src/client/core/pathSerializer.gen.ts new file mode 100644 index 0000000..8d99931 --- /dev/null +++ b/client/cms/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/client/cms/src/client/core/queryKeySerializer.gen.ts b/client/cms/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..d3bb683 --- /dev/null +++ b/client/cms/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/client/cms/src/client/core/serverSentEvents.gen.ts b/client/cms/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..343d25a --- /dev/null +++ b/client/cms/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,266 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/client/cms/src/client/core/types.gen.ts b/client/cms/src/client/core/types.gen.ts new file mode 100644 index 0000000..643c070 --- /dev/null +++ b/client/cms/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/client/cms/src/client/core/utils.gen.ts b/client/cms/src/client/core/utils.gen.ts new file mode 100644 index 0000000..0b5389d --- /dev/null +++ b/client/cms/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/client/cms/src/client/index.ts b/client/cms/src/client/index.ts new file mode 100644 index 0000000..e4e494e --- /dev/null +++ b/client/cms/src/client/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getUserFull, getUserInfo, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from './sdk.gen'; +export type { ClientOptions, DataUser, DataUserSearchDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetUserFullData, GetUserFullError, GetUserFullErrors, GetUserFullResponse, GetUserFullResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceEventInfoResponse, ServiceUserUserInfoData, ServiceUserUserTableResponse, UtilsRespStatus } from './types.gen'; diff --git a/client/cms/src/client/sdk.gen.ts b/client/cms/src/client/sdk.gen.ts new file mode 100644 index 0000000..d9d3e85 --- /dev/null +++ b/client/cms/src/client/sdk.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetUserFullData, GetUserFullErrors, GetUserFullResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses } from './types.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Exchange Auth Code + * + * Exchanges client credentials and user session for a specific redirect authorization code. + */ +export const postAuthExchange = (options: Options) => (options.client ?? client).post({ + url: '/auth/exchange', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Request Magic Link + * + * Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. + */ +export const postAuthMagic = (options: Options) => (options.client ?? client).post({ + url: '/auth/magic', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Handle Auth Callback and Redirect + * + * Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. + */ +export const getAuthRedirect = (options: Options) => (options.client ?? client).get({ url: '/auth/redirect', ...options }); + +/** + * Refresh Access Token + * + * Accepts a valid refresh token to issue a new access token and a rotated refresh token. + */ +export const postAuthRefresh = (options: Options) => (options.client ?? client).post({ + url: '/auth/refresh', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Exchange Code for Token + * + * Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). + */ +export const postAuthToken = (options: Options) => (options.client ?? client).post({ + url: '/auth/token', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Generate Check-in Code + * + * Creates a temporary check-in code for the authenticated user and event. + */ +export const getEventCheckin = (options: Options) => (options.client ?? client).get({ url: '/event/checkin', ...options }); + +/** + * Query Check-in Status + * + * Returns the timestamp of when the user checked in, or null if not yet checked in. + */ +export const getEventCheckinQuery = (options: Options) => (options.client ?? client).get({ url: '/event/checkin/query', ...options }); + +/** + * Submit Check-in Code + * + * Submits the generated code to mark the user as attended. + */ +export const postEventCheckinSubmit = (options: Options) => (options.client ?? client).post({ + url: '/event/checkin/submit', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Get Event Information + * + * Fetches the name, start time, and end time of an event using its UUID. + */ +export const getEventInfo = (options: Options) => (options.client ?? client).get({ url: '/event/info', ...options }); + +/** + * Get Full User Table + * + * Fetches all user records without pagination. This is typically used for administrative overview or data export. + */ +export const getUserFull = (options?: Options) => (options?.client ?? client).get({ url: '/user/full', ...options }); + +/** + * Get My User Information + * + * Fetches the complete profile data for the user associated with the provided session/token. + */ +export const getUserInfo = (options?: Options) => (options?.client ?? client).get({ url: '/user/info', ...options }); + +/** + * List Users + * + * Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. + */ +export const getUserList = (options: Options) => (options.client ?? client).get({ url: '/user/list', ...options }); + +/** + * Update User Information + * + * Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). + * Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). + */ +export const patchUserUpdate = (options: Options) => (options.client ?? client).patch({ + url: '/user/update', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); diff --git a/client/cms/src/client/types.gen.ts b/client/cms/src/client/types.gen.ts new file mode 100644 index 0000000..4e66492 --- /dev/null +++ b/client/cms/src/client/types.gen.ts @@ -0,0 +1,713 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: 'http://localhost:8000/api/v1' | 'https://localhost:8000/api/v1' | (string & {}); +}; + +export type DataUser = { + allow_public?: boolean; + avatar?: string; + bio?: string; + email?: string; + id?: number; + nickname?: string; + permission_level?: number; + subtitle?: string; + user_id?: string; + username?: string; + uuid?: string; +}; + +export type DataUserSearchDoc = { + avatar?: string; + email?: string; + nickname?: string; + subtitle?: string; + type?: string; + user_id?: string; + username?: string; +}; + +export type ServiceAuthExchangeData = { + client_id?: string; + redirect_uri?: string; + state?: string; +}; + +export type ServiceAuthExchangeResponse = { + redirect_uri?: string; +}; + +export type ServiceAuthMagicData = { + client_id?: string; + client_ip?: string; + email?: string; + redirect_uri?: string; + state?: string; + turnstile_token?: string; +}; + +export type ServiceAuthMagicResponse = { + uri?: string; +}; + +export type ServiceAuthRefreshData = { + refresh_token?: string; +}; + +export type ServiceAuthTokenData = { + code?: string; +}; + +export type ServiceAuthTokenResponse = { + access_token?: string; + refresh_token?: string; +}; + +export type ServiceEventCheckinQueryResponse = { + checkin_at?: string; +}; + +export type ServiceEventCheckinResponse = { + checkin_code?: string; +}; + +export type ServiceEventCheckinSubmitData = { + checkin_code?: string; +}; + +export type ServiceEventInfoResponse = { + end_time?: string; + name?: string; + start_time?: string; +}; + +export type ServiceUserUserInfoData = { + allow_public?: boolean; + avatar?: string; + bio?: string; + email?: string; + nickname?: string; + permission_level?: number; + subtitle?: string; + user_id?: string; + username?: string; +}; + +export type ServiceUserUserTableResponse = { + user_table?: Array; +}; + +export type UtilsRespStatus = { + code?: number; + data?: unknown; + error_id?: string; + status?: string; +}; + +export type PostAuthExchangeData = { + /** + * Exchange Request Credentials + */ + body: ServiceAuthExchangeData; + path?: never; + query?: never; + url: '/auth/exchange'; +}; + +export type PostAuthExchangeErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Unauthorized + */ + 401: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthExchangeError = PostAuthExchangeErrors[keyof PostAuthExchangeErrors]; + +export type PostAuthExchangeResponses = { + /** + * Successful exchange + */ + 200: UtilsRespStatus & { + data?: ServiceAuthExchangeResponse; + }; +}; + +export type PostAuthExchangeResponse = PostAuthExchangeResponses[keyof PostAuthExchangeResponses]; + +export type PostAuthMagicData = { + /** + * Magic Link Request Data + */ + body: ServiceAuthMagicData; + path?: never; + query?: never; + url: '/auth/magic'; +}; + +export type PostAuthMagicErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Turnstile Verification Failed + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthMagicError = PostAuthMagicErrors[keyof PostAuthMagicErrors]; + +export type PostAuthMagicResponses = { + /** + * Successful request + */ + 200: UtilsRespStatus & { + data?: ServiceAuthMagicResponse; + }; +}; + +export type PostAuthMagicResponse = PostAuthMagicResponses[keyof PostAuthMagicResponses]; + +export type GetAuthRedirectData = { + body?: never; + path?: never; + query: { + /** + * Client Identifier + */ + client_id: string; + /** + * Target Redirect URI + */ + redirect_uri: string; + /** + * Temporary Verification Code + */ + code: string; + /** + * Opaque state used to maintain state between the request and callback + */ + state?: string; + }; + url: '/auth/redirect'; +}; + +export type GetAuthRedirectErrors = { + /** + * Invalid Input / Client Not Found / URI Mismatch + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Invalid or Expired Verification Code + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetAuthRedirectError = GetAuthRedirectErrors[keyof GetAuthRedirectErrors]; + +export type PostAuthRefreshData = { + /** + * Refresh Token Body + */ + body: ServiceAuthRefreshData; + path?: never; + query?: never; + url: '/auth/refresh'; +}; + +export type PostAuthRefreshErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Invalid Refresh Token + */ + 401: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthRefreshError = PostAuthRefreshErrors[keyof PostAuthRefreshErrors]; + +export type PostAuthRefreshResponses = { + /** + * Successful rotation + */ + 200: UtilsRespStatus & { + data?: ServiceAuthTokenResponse; + }; +}; + +export type PostAuthRefreshResponse = PostAuthRefreshResponses[keyof PostAuthRefreshResponses]; + +export type PostAuthTokenData = { + /** + * Token Request Body + */ + body: ServiceAuthTokenData; + path?: never; + query?: never; + url: '/auth/token'; +}; + +export type PostAuthTokenErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Invalid or Expired Code + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostAuthTokenError = PostAuthTokenErrors[keyof PostAuthTokenErrors]; + +export type PostAuthTokenResponses = { + /** + * Successful token issuance + */ + 200: UtilsRespStatus & { + data?: ServiceAuthTokenResponse; + }; +}; + +export type PostAuthTokenResponse = PostAuthTokenResponses[keyof PostAuthTokenResponses]; + +export type GetEventCheckinData = { + body?: never; + path?: never; + query: { + /** + * Event UUID + */ + event_id: string; + }; + url: '/event/checkin'; +}; + +export type GetEventCheckinErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetEventCheckinError = GetEventCheckinErrors[keyof GetEventCheckinErrors]; + +export type GetEventCheckinResponses = { + /** + * Successfully generated code + */ + 200: UtilsRespStatus & { + data?: ServiceEventCheckinResponse; + }; +}; + +export type GetEventCheckinResponse = GetEventCheckinResponses[keyof GetEventCheckinResponses]; + +export type GetEventCheckinQueryData = { + body?: never; + path?: never; + query: { + /** + * Event UUID + */ + event_id: string; + }; + url: '/event/checkin/query'; +}; + +export type GetEventCheckinQueryErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Record Not Found + */ + 404: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetEventCheckinQueryError = GetEventCheckinQueryErrors[keyof GetEventCheckinQueryErrors]; + +export type GetEventCheckinQueryResponses = { + /** + * Current attendance status + */ + 200: UtilsRespStatus & { + data?: ServiceEventCheckinQueryResponse; + }; +}; + +export type GetEventCheckinQueryResponse = GetEventCheckinQueryResponses[keyof GetEventCheckinQueryResponses]; + +export type PostEventCheckinSubmitData = { + /** + * Checkin Code Data + */ + body: ServiceEventCheckinSubmitData; + path?: never; + query?: never; + url: '/event/checkin/submit'; +}; + +export type PostEventCheckinSubmitErrors = { + /** + * Invalid Code or Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostEventCheckinSubmitError = PostEventCheckinSubmitErrors[keyof PostEventCheckinSubmitErrors]; + +export type PostEventCheckinSubmitResponses = { + /** + * Attendance marked successfully + */ + 200: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostEventCheckinSubmitResponse = PostEventCheckinSubmitResponses[keyof PostEventCheckinSubmitResponses]; + +export type GetEventInfoData = { + body?: never; + path?: never; + query: { + /** + * Event UUID + */ + event_id: string; + }; + url: '/event/info'; +}; + +export type GetEventInfoErrors = { + /** + * Invalid Input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Event Not Found + */ + 404: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetEventInfoError = GetEventInfoErrors[keyof GetEventInfoErrors]; + +export type GetEventInfoResponses = { + /** + * Successful retrieval + */ + 200: UtilsRespStatus & { + data?: ServiceEventInfoResponse; + }; +}; + +export type GetEventInfoResponse = GetEventInfoResponses[keyof GetEventInfoResponses]; + +export type GetUserFullData = { + body?: never; + path?: never; + query?: never; + url: '/user/full'; +}; + +export type GetUserFullErrors = { + /** + * Internal Server Error (Database Error) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetUserFullError = GetUserFullErrors[keyof GetUserFullErrors]; + +export type GetUserFullResponses = { + /** + * Successful retrieval of full user table + */ + 200: UtilsRespStatus & { + data?: ServiceUserUserTableResponse; + }; +}; + +export type GetUserFullResponse = GetUserFullResponses[keyof GetUserFullResponses]; + +export type GetUserInfoData = { + body?: never; + path?: never; + query?: never; + url: '/user/info'; +}; + +export type GetUserInfoErrors = { + /** + * Missing User ID / Unauthorized + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * User Not Found + */ + 404: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error (UUID Parse Failed) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetUserInfoError = GetUserInfoErrors[keyof GetUserInfoErrors]; + +export type GetUserInfoResponses = { + /** + * Successful profile retrieval + */ + 200: UtilsRespStatus & { + data?: ServiceUserUserInfoData; + }; +}; + +export type GetUserInfoResponse = GetUserInfoResponses[keyof GetUserInfoResponses]; + +export type GetUserListData = { + body?: never; + path?: never; + query: { + /** + * Maximum number of users to return (default 0) + */ + limit?: string; + /** + * Number of users to skip + */ + offset: string; + }; + url: '/user/list'; +}; + +export type GetUserListErrors = { + /** + * Invalid Input (Format Error) + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error (Search Engine or Missing Offset) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type GetUserListError = GetUserListErrors[keyof GetUserListErrors]; + +export type GetUserListResponses = { + /** + * Successful paginated list retrieval + */ + 200: UtilsRespStatus & { + data?: Array; + }; +}; + +export type GetUserListResponse = GetUserListResponses[keyof GetUserListResponses]; + +export type PatchUserUpdateData = { + /** + * Updated User Profile Data + */ + body: ServiceUserUserInfoData; + path?: never; + query?: never; + url: '/user/update'; +}; + +export type PatchUserUpdateErrors = { + /** + * Invalid Input (Validation Failed) + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Missing User ID / Unauthorized + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error (Database Error / UUID Parse Failed) + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PatchUserUpdateError = PatchUserUpdateErrors[keyof PatchUserUpdateErrors]; + +export type PatchUserUpdateResponses = { + /** + * Successful profile update + */ + 200: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PatchUserUpdateResponse = PatchUserUpdateResponses[keyof PatchUserUpdateResponses]; diff --git a/client/cms/src/client/zod.gen.ts b/client/cms/src/client/zod.gen.ts new file mode 100644 index 0000000..efd557b --- /dev/null +++ b/client/cms/src/client/zod.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zDataUser = z.object({ + allow_public: z.optional(z.boolean()), + avatar: z.optional(z.string()), + bio: z.optional(z.string()), + email: z.optional(z.string()), + id: z.optional(z.int()), + nickname: z.optional(z.string()), + permission_level: z.optional(z.int()), + subtitle: z.optional(z.string()), + user_id: z.optional(z.string()), + username: z.optional(z.string()), + uuid: z.optional(z.string()) +}); + +export const zDataUserSearchDoc = z.object({ + avatar: z.optional(z.string()), + email: z.optional(z.string()), + nickname: z.optional(z.string()), + subtitle: z.optional(z.string()), + type: z.optional(z.string()), + user_id: z.optional(z.string()), + username: z.optional(z.string()) +}); + +export const zServiceAuthExchangeData = z.object({ + client_id: z.optional(z.string()), + redirect_uri: z.optional(z.string()), + state: z.optional(z.string()) +}); + +export const zServiceAuthExchangeResponse = z.object({ + redirect_uri: z.optional(z.string()) +}); + +export const zServiceAuthMagicData = z.object({ + client_id: z.optional(z.string()), + client_ip: z.optional(z.string()), + email: z.optional(z.string()), + redirect_uri: z.optional(z.string()), + state: z.optional(z.string()), + turnstile_token: z.optional(z.string()) +}); + +export const zServiceAuthMagicResponse = z.object({ + uri: z.optional(z.string()) +}); + +export const zServiceAuthRefreshData = z.object({ + refresh_token: z.optional(z.string()) +}); + +export const zServiceAuthTokenData = z.object({ + code: z.optional(z.string()) +}); + +export const zServiceAuthTokenResponse = z.object({ + access_token: z.optional(z.string()), + refresh_token: z.optional(z.string()) +}); + +export const zServiceEventCheckinQueryResponse = z.object({ + checkin_at: z.optional(z.string()) +}); + +export const zServiceEventCheckinResponse = z.object({ + checkin_code: z.optional(z.string()) +}); + +export const zServiceEventCheckinSubmitData = z.object({ + checkin_code: z.optional(z.string()) +}); + +export const zServiceEventInfoResponse = z.object({ + end_time: z.optional(z.string()), + name: z.optional(z.string()), + start_time: z.optional(z.string()) +}); + +export const zServiceUserUserInfoData = z.object({ + allow_public: z.optional(z.boolean()), + avatar: z.optional(z.string()), + bio: z.optional(z.string()), + email: z.optional(z.string()), + nickname: z.optional(z.string()), + permission_level: z.optional(z.int()), + subtitle: z.optional(z.string()), + user_id: z.optional(z.string()), + username: z.optional(z.string()) +}); + +export const zServiceUserUserTableResponse = z.object({ + user_table: z.optional(z.array(zDataUser)) +}); + +export const zUtilsRespStatus = z.object({ + code: z.optional(z.int()), + data: z.optional(z.unknown()), + error_id: z.optional(z.string()), + status: z.optional(z.string()) +}); + +export const zPostAuthExchangeData = z.object({ + body: zServiceAuthExchangeData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful exchange + */ +export const zPostAuthExchangeResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthExchangeResponse) +})); + +export const zPostAuthMagicData = z.object({ + body: zServiceAuthMagicData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful request + */ +export const zPostAuthMagicResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthMagicResponse) +})); + +export const zGetAuthRedirectData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + client_id: z.string(), + redirect_uri: z.string(), + code: z.string(), + state: z.optional(z.string()) + }) +}); + +export const zPostAuthRefreshData = z.object({ + body: zServiceAuthRefreshData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful rotation + */ +export const zPostAuthRefreshResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthTokenResponse) +})); + +export const zPostAuthTokenData = z.object({ + body: zServiceAuthTokenData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful token issuance + */ +export const zPostAuthTokenResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceAuthTokenResponse) +})); + +export const zGetEventCheckinData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + event_id: z.string() + }) +}); + +/** + * Successfully generated code + */ +export const zGetEventCheckinResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceEventCheckinResponse) +})); + +export const zGetEventCheckinQueryData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + event_id: z.string() + }) +}); + +/** + * Current attendance status + */ +export const zGetEventCheckinQueryResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceEventCheckinQueryResponse) +})); + +export const zPostEventCheckinSubmitData = z.object({ + body: zServiceEventCheckinSubmitData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Attendance marked successfully + */ +export const zPostEventCheckinSubmitResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.record(z.string(), z.unknown())) +})); + +export const zGetEventInfoData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + event_id: z.string() + }) +}); + +/** + * Successful retrieval + */ +export const zGetEventInfoResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceEventInfoResponse) +})); + +export const zGetUserFullData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful retrieval of full user table + */ +export const zGetUserFullResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceUserUserTableResponse) +})); + +export const zGetUserInfoData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful profile retrieval + */ +export const zGetUserInfoResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceUserUserInfoData) +})); + +export const zGetUserListData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.object({ + limit: z.optional(z.string()), + offset: z.string() + }) +}); + +/** + * Successful paginated list retrieval + */ +export const zGetUserListResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.array(zDataUserSearchDoc)) +})); + +export const zPatchUserUpdateData = z.object({ + body: zServiceUserUserInfoData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful profile update + */ +export const zPatchUserUpdateResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.record(z.string(), z.unknown())) +})); diff --git a/client/cms/src/components/checkin/qr-dialog.tsx b/client/cms/src/components/checkin/qr-dialog.tsx index f4bb318..a1fa442 100644 --- a/client/cms/src/components/checkin/qr-dialog.tsx +++ b/client/cms/src/components/checkin/qr-dialog.tsx @@ -9,7 +9,6 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { QRCode } from '@/components/ui/shadcn-io/qr-code'; -import { useCheckinCode } from '@/hooks/data/useGetCheckInCode'; import { Button } from '../ui/button'; export function QrDialog( @@ -35,23 +34,24 @@ export function QrDialog( } function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) { - const { data } = useCheckinCode(eventId, enabled); + // const { data } = useCheckinCode(eventId, enabled); + const data = { data: { checkin_code: `dummy${eventId}${enabled}` } }; return data ? ( - <> -
- + <> +
+ +
+ +
+ {data.data.checkin_code}
- -
- {data.data.checkin_code} -
-
- - ) +
+ + ) : ( - - ); + + ); } function QrSectionSkeleton() { diff --git a/client/cms/src/components/login-form.tsx b/client/cms/src/components/login-form.tsx index 8ef955b..5447440 100644 --- a/client/cms/src/components/login-form.tsx +++ b/client/cms/src/components/login-form.tsx @@ -32,7 +32,7 @@ export function LoginForm({ event.preventDefault(); const formData = new FormData(formRef.current!); const email = formData.get('email')! as string; - mutateAsync({ email, turnstile_token: token!, ...oauthParams }).then(() => { + mutateAsync({ body: { email, turnstile_token: token!, ...oauthParams } }).then(() => { void navigate({ to: '/magicLinkSent', search: { email } }); }).catch((error) => { console.error(error); diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile-dialog.tsx index e9efd34..c98d4c7 100644 --- a/client/cms/src/components/profile/edit-profile-dialog.tsx +++ b/client/cms/src/components/profile/edit-profile-dialog.tsx @@ -29,7 +29,8 @@ const formSchema = z.object({ avatar: z.url().min(1), }); export function EditProfileDialog() { - const { data: user } = useUserInfo(); + const { data } = useUserInfo(); + const user = data.data!; const { mutateAsync } = useUpdateUser(); const form = useForm({ @@ -46,7 +47,7 @@ export function EditProfileDialog() { value, }) => { try { - await mutateAsync(value); + await mutateAsync({ body: value }); toast.success('个人资料更新成功'); } catch (error) { diff --git a/client/cms/src/components/profile/main-profile.tsx b/client/cms/src/components/profile/main-profile.tsx index 573f3e5..425d632 100644 --- a/client/cms/src/components/profile/main-profile.tsx +++ b/client/cms/src/components/profile/main-profile.tsx @@ -12,8 +12,9 @@ import { Button } from '../ui/button'; import { EditProfileDialog } from './edit-profile-dialog'; export function MainProfile() { - const { data: user } = useUserInfo(); - const [bio, setBio] = useState(() => base64ToUtf8(user.bio)); + const { data } = useUserInfo(); + const user = data.data!; + const [bio, setBio] = useState(() => base64ToUtf8(user.bio ?? '')); const [enableBioEdit, setEnableBioEdit] = useState(false); const { mutateAsync } = useUpdateUser(); @@ -61,7 +62,7 @@ export function MainProfile() { else { if (!isNil(bio)) { try { - await mutateAsync({ bio: utf8ToBase64(bio) }); + await mutateAsync({ body: { bio: utf8ToBase64(bio) } }); setEnableBioEdit(false); } catch (error) { diff --git a/client/cms/src/components/sidebar/nav-user.tsx b/client/cms/src/components/sidebar/nav-user.tsx index 0917530..3da5d56 100644 --- a/client/cms/src/components/sidebar/nav-user.tsx +++ b/client/cms/src/components/sidebar/nav-user.tsx @@ -29,7 +29,8 @@ import { Skeleton } from '../ui/skeleton'; function NavUser_() { const { isMobile } = useSidebar(); - const { data: user } = useUserInfo(); + const { data } = useUserInfo(); + const user = data.data!; const { logout } = useLogout(); return ( diff --git a/client/cms/src/hooks/data/useExchangeToken.ts b/client/cms/src/hooks/data/useExchangeToken.ts new file mode 100644 index 0000000..99c5df8 --- /dev/null +++ b/client/cms/src/hooks/data/useExchangeToken.ts @@ -0,0 +1,16 @@ +import { postAuthExchangeMutation } from "@/client/@tanstack/react-query.gen"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; + +export function useExchangeToken() { + return useMutation({ + ...postAuthExchangeMutation(), + onSuccess: (data) => { + window.location.href = data.data?.redirect_uri!; + }, + onError: (error) => { + console.error(error); + toast("An error occurred while exchanging the token. Please login manually."); + } + }) +} diff --git a/client/cms/src/hooks/data/useGetCheckInCode.ts b/client/cms/src/hooks/data/useGetCheckInCode.ts deleted file mode 100644 index e4d5f05..0000000 --- a/client/cms/src/hooks/data/useGetCheckInCode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -export function useCheckinCode(eventId: string, enabled: boolean) { - return useQuery({ - queryKey: ['getCheckinCode', eventId], - queryFn: async () => { - return axiosClient.get<{ - checkin_code: string; - }>('/user/checkin', { - params: { - event_id: eventId, - }, - }); - }, - enabled, - }); -} diff --git a/client/cms/src/hooks/data/useGetMagicLink.ts b/client/cms/src/hooks/data/useGetMagicLink.ts index 4be1b04..0fc0e61 100644 --- a/client/cms/src/hooks/data/useGetMagicLink.ts +++ b/client/cms/src/hooks/data/useGetMagicLink.ts @@ -1,16 +1,8 @@ -import type { AuthorizeSearchParams } from '@/routes/authorize'; +import { postAuthMagicMutation } from '@/client/@tanstack/react-query.gen'; import { useMutation } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -interface GetMagicLinkPayload extends AuthorizeSearchParams { - email: string; - turnstile_token: string; -} export function useGetMagicLink() { return useMutation({ - mutationFn: async (payload: GetMagicLinkPayload) => { - return axiosClient.post<{ status: string }>('/auth/magic', payload); - }, - }); + ...postAuthMagicMutation() + }) } diff --git a/client/cms/src/hooks/data/useUpdateUser.ts b/client/cms/src/hooks/data/useUpdateUser.ts index 8958939..6988dca 100644 --- a/client/cms/src/hooks/data/useUpdateUser.ts +++ b/client/cms/src/hooks/data/useUpdateUser.ts @@ -1,22 +1,12 @@ +import { getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen'; 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); - }, + ...patchUserUpdateMutation(), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['userInfo'] }); + await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() }); }, - }); + }) } diff --git a/client/cms/src/hooks/data/useUserInfo.ts b/client/cms/src/hooks/data/useUserInfo.ts index 3f8394c..fde0174 100644 --- a/client/cms/src/hooks/data/useUserInfo.ts +++ b/client/cms/src/hooks/data/useUserInfo.ts @@ -1,23 +1,9 @@ +import { getUserInfoOptions } from '@/client/@tanstack/react-query.gen'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; export function useUserInfo() { return useSuspenseQuery({ - queryKey: ['userInfo'], - queryFn: async () => { - const response = await axiosClient.get<{ - username: string; - user_id: string; - email: string; - type: string; - nickname: string; - subtitle: string; - avatar: string; - bio: string; - } - >('/user/info'); - return response.data; - }, - staleTime: 10 * 60 * 1000, + ...getUserInfoOptions(), + staleTime: 10 * 60 * 1000 }); } diff --git a/client/cms/src/hooks/data/useValidateMagicLink.ts b/client/cms/src/hooks/data/useValidateMagicLink.ts deleted file mode 100644 index 7f59477..0000000 --- a/client/cms/src/hooks/data/useValidateMagicLink.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import { axiosClient } from '@/lib/axios'; - -export function useValidateMagicLink(ticket: string) { - return useSuspenseQuery({ - queryKey: ['validateMagicLink', ticket], - queryFn: async () => { - return axiosClient.get<{ access_token: string; refresh_token: string }>('/auth/magic/verify', { params: { token: ticket } }); - }, - }); -} diff --git a/client/cms/src/lib/axios.ts b/client/cms/src/lib/axios.ts deleted file mode 100644 index f4d8db5..0000000 --- a/client/cms/src/lib/axios.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { AxiosError, AxiosRequestConfig } from 'axios'; -import type { JsonValue } from 'type-fest'; -import axios from 'axios'; -import { isNil } from 'lodash-es'; -import { router } from '@/lib/router'; -import { clearTokens, doRefreshToken, getRefreshToken, getToken, setRefreshToken, setToken } from './token'; - -export const HEADER_API_VERSION = { - 'X-Api-Version': 'latest', -}; - -export const axiosClient = axios.create({ - baseURL: '/api/v1/', - headers: HEADER_API_VERSION, -}); - -axiosClient.interceptors.request.use((config) => { - const token = getToken(); - if (token !== null) { - config.headers = config.headers ?? {}; - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - -type RetryConfig = AxiosRequestConfig & { _retry?: boolean }; - -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; - if (!error.response || error.response.status !== 401 || !originalRequest) { - return Promise.reject(error); - } - if (error.response.status === 401 && originalRequest.url !== '/auth/refresh' && !isNil(getRefreshToken())) { - try { - const maybeRefreshTokenResponse = await doRefreshToken(); - if (maybeRefreshTokenResponse.status !== 200) { - throw new Error('Failed to refresh token'); - } - const { access_token, refresh_token } = maybeRefreshTokenResponse.data; - originalRequest.headers = originalRequest.headers ?? {}; - originalRequest.headers.Authorization = `Bearer ${access_token}`; - setToken(access_token); - setRefreshToken(refresh_token); - return await axiosClient(originalRequest); - } - // eslint-disable-next-line unused-imports/no-unused-vars - catch (e) { - // Should remove token (tokens are out of date) - clearTokens(); - await router.navigate({ to: '/authorize' }); - } - } -}); diff --git a/client/cms/src/lib/client.ts b/client/cms/src/lib/client.ts new file mode 100644 index 0000000..be66d6d --- /dev/null +++ b/client/cms/src/lib/client.ts @@ -0,0 +1,63 @@ +import { + getToken, + getRefreshToken, + setToken, + setRefreshToken, + clearTokens, + doRefreshToken +} from "./token"; +import { router } from "./router"; +import { isEmpty, + isNil } from "lodash-es"; +import { client } from "@/client/client.gen"; + +export function configInternalApiClient() { + client.setConfig({ + baseUrl: '/api/v1/', + headers: { + 'X-Api-Version': 'latest', + }, + }); + + client.interceptors.request.use((request) => { + const token = getToken(); + if (token) { + request.headers.set('Authorization', `Bearer ${token}`); + } + return request; + }); + + client.interceptors.response.use(async (response, request, options) => { + if (response.status === 401) { + const refreshToken = getRefreshToken(); + // Avoid infinite loop if the refresh token request itself fails + if (!request.url.includes('/auth/refresh') && !isNil(refreshToken)) { + try { + const refreshResponse = await doRefreshToken(); + if (!isEmpty(refreshResponse)) { + const { access_token, refresh_token } = refreshResponse; + setToken(access_token!); + setRefreshToken(refresh_token!); + + const fetchFn = options.fetch ?? globalThis.fetch; + const headers = new Headers(request.headers); + headers.set('Authorization', `Bearer ${access_token}`); + + return fetchFn(request.url, { + method: request.method, + headers, + body: (options.serializedBody ?? options.body) as BodyInit | null | undefined, + signal: request.signal, + }); + } + } catch (e) { + clearTokens(); + await router.navigate({ to: '/authorize' }); + return response; + } + } + } + return response; + }); + +} diff --git a/client/cms/src/lib/token.ts b/client/cms/src/lib/token.ts index 9c7c57c..e671f98 100644 --- a/client/cms/src/lib/token.ts +++ b/client/cms/src/lib/token.ts @@ -1,4 +1,4 @@ -import { axiosClient, HEADER_API_VERSION } from './axios'; +import { postAuthRefresh, type ServiceAuthTokenResponse } from '@/client'; export function setToken(token: string) { localStorage.setItem('token', token); @@ -29,18 +29,11 @@ export function clearTokens() { setRefreshToken(''); } -export async function doSetTokenByCode(code: string) { - return new Promise((resolve, reject) => { - axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION }).then(({ data }) => { - setToken(data.access_token); - setRefreshToken(data.refresh_token); - resolve(); - }).catch((error) => { - reject(error); - }); +export async function doRefreshToken(): Promise { + const { data } = await postAuthRefresh({ + body: { + refresh_token: getRefreshToken()! + } }); -} - -export async function doRefreshToken() { - return axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token: getRefreshToken() }, { headers: HEADER_API_VERSION }); + return data?.data; } diff --git a/client/cms/src/main.tsx b/client/cms/src/main.tsx index 0489bf0..ef930e2 100644 --- a/client/cms/src/main.tsx +++ b/client/cms/src/main.tsx @@ -2,6 +2,9 @@ import { RouterProvider } from '@tanstack/react-router'; import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { router } from '@/lib/router'; +import { configInternalApiClient } from './lib/client'; + +configInternalApiClient(); // Render the app const rootElement = document.getElementById('root')!; diff --git a/client/cms/src/routes/authorize.tsx b/client/cms/src/routes/authorize.tsx index 7f3c164..273088c 100644 --- a/client/cms/src/routes/authorize.tsx +++ b/client/cms/src/routes/authorize.tsx @@ -3,9 +3,10 @@ import { zodValidator } from '@tanstack/zod-adapter'; import { isNil } from 'lodash-es'; import z from 'zod'; import { LoginForm } from '@/components/login-form'; -import { axiosClient } from '@/lib/axios'; import { generateOAuthState } from '@/lib/random'; import { getToken } from '@/lib/token'; +import { useExchangeToken } from '@/hooks/data/useExchangeToken'; +import { useEffect } from 'react'; const authorizeSchema = z.object({ response_type: z.literal('code').default('code'), @@ -24,22 +25,21 @@ export const Route = createFileRoute('/authorize')({ function RouteComponent() { const token = getToken(); const oauthParams = Route.useSearch(); + const mutation = useExchangeToken(); /** * Auth by Token Flow */ - if (!isNil(token)) { - axiosClient.post<{ redirect_uri: string }>('/auth/exchange', { - client_id: oauthParams.client_id, - redirect_uri: oauthParams.redirect_uri, - state: oauthParams.state, - }).then((res) => { - window.location.href = res.data.redirect_uri; - }).catch((e) => { - console.error(e); - return 'Token exchange failed'; - }); - return 'Redirecting'; - } + useEffect(() => { + if (!isNil(token) && mutation.isIdle) { + mutation.mutate({ + body: { + client_id: oauthParams.client_id, + redirect_uri: oauthParams.redirect_uri, + state: oauthParams.state, + } + }); + } + }, [token, mutation.isIdle]); return (
diff --git a/client/cms/src/routes/token.tsx b/client/cms/src/routes/token.tsx index 78f8bc7..0f6fdb7 100644 --- a/client/cms/src/routes/token.tsx +++ b/client/cms/src/routes/token.tsx @@ -1,7 +1,11 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { useEffect, useState } from 'react'; +import { + useEffect, + useState } from 'react'; import z from 'zod'; -import { doSetTokenByCode } from '@/lib/token'; +import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen'; +import { useMutation } from '@tanstack/react-query'; +import { setRefreshToken, setToken } from '@/lib/token'; const tokenCodeSchema = z.object({ code: z.string().nonempty(), @@ -17,14 +21,23 @@ function RouteComponent() { const [status, setStatus] = useState('Loading...'); const navigate = useNavigate(); - useEffect(() => { - doSetTokenByCode(code).then(() => { + const mutation = useMutation({ + ...postAuthTokenMutation(), + onSuccess: (data) => { + setToken(data.data?.access_token!) + setRefreshToken(data.data?.refresh_token!) void navigate({ to: '/' }); - }).catch((_) => { + }, + onError: () => { setStatus('Error getting token'); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + } + }) + + useEffect(() => { + if (mutation.isIdle) { + mutation.mutate({ body: { code } }) + } + }, []) return
{status}
; } -- 2.49.1 From 65f86a8156b818d99e6c98661a0da4bf0bb41e95 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 11:48:54 +0800 Subject: [PATCH 028/195] Set test env files Signed-off-by: Asai Neko --- Caddyfile | 9 +++ conpose.yaml | 98 ++++++++++++++++++++++++++++++ container/client-cms.Containerfile | 2 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 Caddyfile create mode 100644 conpose.yaml diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..3e7a53c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,9 @@ +test.nix.org.cn { + handle_path /* { + reverse_proxy client-cms:3000 + } + + handle_path /api/* { + reverse_proxy backend:8000 + } +} diff --git a/conpose.yaml b/conpose.yaml new file mode 100644 index 0000000..e29e998 --- /dev/null +++ b/conpose.yaml @@ -0,0 +1,98 @@ +services: + postgres: + image: docker.io/postgres:18-alpine + container_name: cms-db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: docker.io/redis:8-alpine + container_name: cms-redis + volumes: + - ./data/redis:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 5s + timeout: 3s + retries: 5 + + meilisearch: + image: getmeili/meilisearch:v1.34.3 + container_name: cms-search + environment: + - MEILI_MASTER_KEY=your_master_key + volumes: + - ./data/meilisearch:/meili_data + healthcheck: + test: + ["CMD-SHELL", "curl -f http://localhost:7700/health || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + + lgtm: + image: grafana/otel-lgtm:latest + container_name: lgtm-stack + ports: + - "3000:3000" + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + volumes: + - ./data/lgtm:/data + healthcheck: + test: + [ + "CMD-SHELL", + "curl -f http://localhost:3000/api/health || exit 1", + ] + interval: 10s + timeout: 5s + retries: 5 + + caddy: + image: docker.io/caddy:latest + container_name: caddy-proxy + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ./data/caddy/data:/data + - ./data/caddy/config:/config + depends_on: + - backend + - client-cms + + client-cms: + image: registry.asnk.io/nixcn-cms/client-cms:dev + container_name: cms-client + restart: always + depends_on: + lgtm: + condition: service_healthy + + backend: + image: registry.asnk.io/nixcn-cms/backend:dev + container_name: cms-backend + restart: always + volumes: + - ./config.yaml:/app/config.yaml:ro + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + meilisearch: + condition: service_healthy + lgtm: + condition: service_healthy diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 76a8860..48eec67 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -3,7 +3,7 @@ RUN apk add just RUN npm install -g corepack && \ corepack enable WORKDIR /app -ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL +ENV VITE_APP_BASE_URL=$VITE_APP_BASE_URL COPY . . RUN cd client/cms && pnpm install RUN cd client/cms && pnpm run build --outDir /app/.outputs/client/cms/dist -- 2.49.1 From b8c89fcf5f420b83568d6c16a13e9ed1f96d78fe Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Thu, 29 Jan 2026 11:53:14 +0800 Subject: [PATCH 029/195] refactor(client): tighten env type Signed-off-by: Noa Virellia --- client/cms/src/routes/authorize.tsx | 5 ++++- client/cms/src/vite.env.d.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 client/cms/src/vite.env.d.ts diff --git a/client/cms/src/routes/authorize.tsx b/client/cms/src/routes/authorize.tsx index 273088c..0688fb4 100644 --- a/client/cms/src/routes/authorize.tsx +++ b/client/cms/src/routes/authorize.tsx @@ -8,10 +8,13 @@ import { getToken } from '@/lib/token'; import { useExchangeToken } from '@/hooks/data/useExchangeToken'; import { useEffect } from 'react'; + +const baseUrl = import.meta.env.VITE_APP_BASE_URL; + const authorizeSchema = z.object({ response_type: z.literal('code').default('code'), client_id: z.literal('org_client').default('org_client'), - redirect_uri: z.string().default(`${new URL(import.meta.env.VITE_APP_BASE_URL as string).toString()}token`), + redirect_uri: z.string().default(`${new URL(baseUrl).toString()}token`), state: z.string().default(generateOAuthState()), }); diff --git a/client/cms/src/vite.env.d.ts b/client/cms/src/vite.env.d.ts new file mode 100644 index 0000000..ff2503a --- /dev/null +++ b/client/cms/src/vite.env.d.ts @@ -0,0 +1,11 @@ +interface ViteTypeOptions { + strictImportMetaEnv: unknown +} + +interface ImportMetaEnv { + readonly VITE_APP_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} -- 2.49.1 From 5ece89268f0797f156a8191d18e7af5f12937d88 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 11:59:00 +0800 Subject: [PATCH 030/195] Add ARG for client-cms Containerfile Signed-off-by: Asai Neko --- container/client-cms.Containerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 48eec67..0008d56 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -3,6 +3,7 @@ RUN apk add just RUN npm install -g corepack && \ corepack enable WORKDIR /app +ARG VITE_APP_BASE_URL ENV VITE_APP_BASE_URL=$VITE_APP_BASE_URL COPY . . RUN cd client/cms && pnpm install -- 2.49.1 From fabba842ce30d143e5b5db1ff5f34b7051fbbf60 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 12:52:32 +0800 Subject: [PATCH 031/195] Deploy client-cms to caddy container Signed-off-by: Asai Neko --- api/auth/exchange.go | 1 + api/auth/magic.go | 1 + api/auth/redirect.go | 1 + api/auth/refresh.go | 1 + api/auth/token.go | 1 + api/event/checkin.go | 3 +++ api/event/info.go | 1 + api/user/full.go | 1 + api/user/info.go | 1 + api/user/list.go | 1 + api/user/update.go | 1 + container/client-cms.Containerfile | 8 ++++---- Caddyfile => deploy/Caddyfile | 10 +++++----- deploy/client-cms.Caddyfile | 6 ++++++ conpose.yaml => deploy/conpose.yaml | 2 ++ go.mod | 2 +- server/server.go | 24 ++++++++++++------------ 17 files changed, 43 insertions(+), 22 deletions(-) rename Caddyfile => deploy/Caddyfile (69%) create mode 100644 deploy/client-cms.Caddyfile rename conpose.yaml => deploy/conpose.yaml (98%) diff --git a/api/auth/exchange.go b/api/auth/exchange.go index a128108..bc6df80 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -10,6 +10,7 @@ import ( ) // Exchange handles the authorization code swap process. +// // @Summary Exchange Auth Code // @Description Exchanges client credentials and user session for a specific redirect authorization code. // @Tags Authentication diff --git a/api/auth/magic.go b/api/auth/magic.go index 31469c9..35f7e72 100644 --- a/api/auth/magic.go +++ b/api/auth/magic.go @@ -9,6 +9,7 @@ import ( ) // Magic handles the "Magic Link" authentication request. +// // @Summary Request Magic Link // @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. // @Tags Authentication diff --git a/api/auth/redirect.go b/api/auth/redirect.go index aa56dda..7e455ff 100644 --- a/api/auth/redirect.go +++ b/api/auth/redirect.go @@ -9,6 +9,7 @@ import ( ) // Redirect handles the post-verification callback and redirects the user to the target application. +// // @Summary Handle Auth Callback and Redirect // @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. // @Tags Authentication diff --git a/api/auth/refresh.go b/api/auth/refresh.go index 8205108..a032c9b 100644 --- a/api/auth/refresh.go +++ b/api/auth/refresh.go @@ -9,6 +9,7 @@ import ( ) // Refresh handles the token rotation process. +// // @Summary Refresh Access Token // @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token. // @Tags Authentication diff --git a/api/auth/token.go b/api/auth/token.go index c8257bb..e6294bd 100644 --- a/api/auth/token.go +++ b/api/auth/token.go @@ -9,6 +9,7 @@ import ( ) // Token exchanges an authorization code for access and refresh tokens. +// // @Summary Exchange Code for Token // @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). // @Tags Authentication diff --git a/api/event/checkin.go b/api/event/checkin.go index 3c25084..e98ab6a 100644 --- a/api/event/checkin.go +++ b/api/event/checkin.go @@ -10,6 +10,7 @@ import ( ) // Checkin generates a check-in code for a specific event. +// // @Summary Generate Check-in Code // @Description Creates a temporary check-in code for the authenticated user and event. // @Tags Event @@ -49,6 +50,7 @@ func (self *EventHandler) Checkin(c *gin.Context) { } // CheckinSubmit validates a check-in code to complete attendance. +// // @Summary Submit Check-in Code // @Description Submits the generated code to mark the user as attended. // @Tags Event @@ -82,6 +84,7 @@ func (self *EventHandler) CheckinSubmit(c *gin.Context) { } // CheckinQuery retrieves the check-in status of a user for an event. +// // @Summary Query Check-in Status // @Description Returns the timestamp of when the user checked in, or null if not yet checked in. // @Tags Event diff --git a/api/event/info.go b/api/event/info.go index 232059c..6f63a6a 100644 --- a/api/event/info.go +++ b/api/event/info.go @@ -10,6 +10,7 @@ import ( ) // Info retrieves basic information about a specific event. +// // @Summary Get Event Information // @Description Fetches the name, start time, and end time of an event using its UUID. // @Tags Event diff --git a/api/user/full.go b/api/user/full.go index a94b6d7..b376836 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -9,6 +9,7 @@ import ( ) // Full retrieves the complete list of users directly from the database table. +// // @Summary Get Full User Table // @Description Fetches all user records without pagination. This is typically used for administrative overview or data export. // @Tags User diff --git a/api/user/info.go b/api/user/info.go index 70b42e0..b22faa8 100644 --- a/api/user/info.go +++ b/api/user/info.go @@ -10,6 +10,7 @@ import ( ) // Info retrieves the profile information of the currently authenticated user. +// // @Summary Get My User Information // @Description Fetches the complete profile data for the user associated with the provided session/token. // @Tags User diff --git a/api/user/list.go b/api/user/list.go index bc3dc7b..8baf8df 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -9,6 +9,7 @@ import ( ) // List retrieves a paginated list of users from the search engine. +// // @Summary List Users // @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance. // @Tags User diff --git a/api/user/update.go b/api/user/update.go index 68989f4..d6d3513 100644 --- a/api/user/update.go +++ b/api/user/update.go @@ -10,6 +10,7 @@ import ( ) // Update modifies the profile information for the currently authenticated user. +// // @Summary Update User Information // @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). // @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). diff --git a/container/client-cms.Containerfile b/container/client-cms.Containerfile index 0008d56..a49cfef 100644 --- a/container/client-cms.Containerfile +++ b/container/client-cms.Containerfile @@ -9,8 +9,8 @@ COPY . . RUN cd client/cms && pnpm install RUN cd client/cms && pnpm run build --outDir /app/.outputs/client/cms/dist -FROM docker.io/busybox:1.37 -WORKDIR /app -COPY --from=client-cms-build /app/.outputs/client/cms/dist . +FROM docker.io/caddy:2-alpine +COPY ./deploy/client-cms.Caddyfile /etc/caddy/Caddyfile +COPY --from=client-cms-build /app/.outputs/client/cms/dist /srv EXPOSE 3000 -ENTRYPOINT ["httpd", "-f", "-p", "3000", "-h", "/app", "-v"] +ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/Caddyfile b/deploy/Caddyfile similarity index 69% rename from Caddyfile rename to deploy/Caddyfile index 3e7a53c..cd3eae6 100644 --- a/Caddyfile +++ b/deploy/Caddyfile @@ -1,9 +1,9 @@ test.nix.org.cn { - handle_path /* { - reverse_proxy client-cms:3000 - } - - handle_path /api/* { + handle /api/* { reverse_proxy backend:8000 } + + handle { + reverse_proxy client-cms:3000 + } } diff --git a/deploy/client-cms.Caddyfile b/deploy/client-cms.Caddyfile new file mode 100644 index 0000000..dfdd65a --- /dev/null +++ b/deploy/client-cms.Caddyfile @@ -0,0 +1,6 @@ +:3000 { + root * /srv + encode zstd gzip + try_files {path} /index.html + file_server +} diff --git a/conpose.yaml b/deploy/conpose.yaml similarity index 98% rename from conpose.yaml rename to deploy/conpose.yaml index e29e998..98f82bc 100644 --- a/conpose.yaml +++ b/deploy/conpose.yaml @@ -8,6 +8,8 @@ services: POSTGRES_DB: postgres volumes: - ./data/postgres:/var/lib/postgresql/data + ports: + - 5432:5432 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] interval: 5s diff --git a/go.mod b/go.mod index 3aa5302..e98cf52 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 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 @@ -112,7 +113,6 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/swag v1.16.6 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect diff --git a/server/server.go b/server/server.go index 61a1285..70d649c 100644 --- a/server/server.go +++ b/server/server.go @@ -18,18 +18,18 @@ import ( _ "nixcn-cms/docs" ) -// @title NixCN CMS API -// @version 1.0 -// @description API Docs based on Gin framework -// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host localhost:8000 -// @BasePath /api/v1 -// @schemes http https +// @title NixCN CMS API +// @version 1.0 +// @description API Docs based on Gin framework +// @termsOfService http://swagger.io/terms/ +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:8000 +// @BasePath /api/v1 +// @schemes http https func Start(ctx context.Context) { if !viper.GetBool("server.debug_mode") { gin.SetMode(gin.ReleaseMode) -- 2.49.1 From 0f1c5b12935ab218872d20f3a14cd2eae0aa3a18 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 13:42:00 +0800 Subject: [PATCH 032/195] Fix new user create 500 error Signed-off-by: Asai Neko --- config.default.yaml | 2 +- data/user.go | 3 --- service/service_auth/redirect.go | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 602c7bb..acfcd01 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -20,7 +20,7 @@ cache: db: 0 service_name: nixcn-cms-redis search: - host: 127.0.0.1 + host: http://127.0.0.1:7700 api_key: "" service_name: nixcn-cms-meilisearch email: diff --git a/data/user.go b/data/user.go index 0494e47..ea7fd30 100644 --- a/data/user.go +++ b/data/user.go @@ -81,9 +81,6 @@ func (self *User) GetByEmail(ctx context.Context, email *string) (*User, error) First(&user).Error if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } return nil, err } diff --git a/service/service_auth/redirect.go b/service/service_auth/redirect.go index 7174386..3c348f1 100644 --- a/service/service_auth/redirect.go +++ b/service/service_auth/redirect.go @@ -56,6 +56,7 @@ func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *Redirec GetByEmail(payload.Context, &authCode.Email) if err != nil { if err == gorm.ErrRecordNotFound { + userData = &data.User{} userData.UUID = uuid.New() userData.UserId = uuid.New() userData.Email = authCode.Email -- 2.49.1 From f27b991d69e76cf93438e95579a07d384fce18f7 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 13:51:51 +0800 Subject: [PATCH 033/195] Fix deploy compose file Signed-off-by: Asai Neko --- deploy/{conpose.yaml => compose.yaml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename deploy/{conpose.yaml => compose.yaml} (98%) diff --git a/deploy/conpose.yaml b/deploy/compose.yaml similarity index 98% rename from deploy/conpose.yaml rename to deploy/compose.yaml index 98f82bc..922b82e 100644 --- a/deploy/conpose.yaml +++ b/deploy/compose.yaml @@ -31,7 +31,7 @@ services: image: getmeili/meilisearch:v1.34.3 container_name: cms-search environment: - - MEILI_MASTER_KEY=your_master_key + - MEILI_MASTER_KEY=meilisearch volumes: - ./data/meilisearch:/meili_data healthcheck: -- 2.49.1 From 83bd6c2830283cc517de162d3391488e41b6d37e Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Thu, 29 Jan 2026 20:23:42 +0800 Subject: [PATCH 034/195] fix(client): relax form schema and validate on submit Signed-off-by: Noa Virellia --- client/cms/src/components/profile/edit-profile-dialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile-dialog.tsx index c98d4c7..8f3bcca 100644 --- a/client/cms/src/components/profile/edit-profile-dialog.tsx +++ b/client/cms/src/components/profile/edit-profile-dialog.tsx @@ -24,9 +24,9 @@ import { useUserInfo } from '@/hooks/data/useUserInfo'; const formSchema = z.object({ username: z.string().min(5), - nickname: z.string().min(1), - subtitle: z.string().min(1), - avatar: z.url().min(1), + nickname: z.string(), + subtitle: z.string(), + avatar: z.url(), }); export function EditProfileDialog() { const { data } = useUserInfo(); @@ -41,7 +41,7 @@ export function EditProfileDialog() { subtitle: user.subtitle, }, validators: { - onBlur: formSchema, + onSubmit: formSchema }, onSubmit: async ({ value, -- 2.49.1 From 12a02d13dcea8123532ef135d1e6014bae48040c Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 20:34:46 +0800 Subject: [PATCH 035/195] Update update_user_info logic in service_user Signed-off-by: Asai Neko --- .../profile/edit-profile-dialog.tsx | 8 +- service/service_user/update_user_info.go | 76 +++++++++---------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile-dialog.tsx index 8f3bcca..c98d4c7 100644 --- a/client/cms/src/components/profile/edit-profile-dialog.tsx +++ b/client/cms/src/components/profile/edit-profile-dialog.tsx @@ -24,9 +24,9 @@ import { useUserInfo } from '@/hooks/data/useUserInfo'; const formSchema = z.object({ username: z.string().min(5), - nickname: z.string(), - subtitle: z.string(), - avatar: z.url(), + nickname: z.string().min(1), + subtitle: z.string().min(1), + avatar: z.url().min(1), }); export function EditProfileDialog() { const { data } = useUserInfo(); @@ -41,7 +41,7 @@ export function EditProfileDialog() { subtitle: user.subtitle, }, validators: { - onSubmit: formSchema + onBlur: formSchema, }, onSubmit: async ({ value, diff --git a/service/service_user/update_user_info.go b/service/service_user/update_user_info.go index 25b7bc0..43b4836 100644 --- a/service/service_user/update_user_info.go +++ b/service/service_user/update_user_info.go @@ -40,50 +40,46 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U 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) + 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: shared.CommonResult{ - HttpCode: 400, - Exception: execption, - }, - Data: nil, - } - - return + result = &UserInfoResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: execption, + }, + Data: nil, } - userData.SetNickname(payload.Data.Nickname) + + 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) + 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: shared.CommonResult{ - HttpCode: 400, - Exception: execption, - }, - Data: nil, - } - - return + result = &UserInfoResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: execption, + }, + Data: nil, } - userData.SetSubtitle(payload.Data.Subtitle) + + return } + userData.SetSubtitle(payload.Data.Subtitle) if payload.Data.Avatar != "" { _, err := url.ParseRequestURI(payload.Data.Avatar) @@ -108,6 +104,8 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U return } userData.SetAvatar(payload.Data.Avatar) + } else if payload.Data.Avatar == "" { + userData.SetAvatar("") } if payload.Data.Bio != "" { @@ -131,7 +129,9 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U return } - userData.Bio = payload.Data.Bio + userData.SetBio(payload.Data.Bio) + } else if payload.Data.Bio == "" { + userData.SetBio("") } err = userData.UpdateByUserID(payload.Context, &payload.UserId) -- 2.49.1 From 9016b21464178fcbbc53b259ecd339b4bcac1cd6 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 21:14:54 +0800 Subject: [PATCH 036/195] Fix service_user logic Signed-off-by: Asai Neko --- data/user.go | 14 +- service/service_user/get_user_info.go | 24 ++-- service/service_user/update_user_info.go | 175 +++++++++++++---------- 3 files changed, 125 insertions(+), 88 deletions(-) diff --git a/data/user.go b/data/user.go index ea7fd30..c5da81e 100644 --- a/data/user.go +++ b/data/user.go @@ -127,12 +127,20 @@ func (self *User) Create(ctx context.Context) error { return nil } -func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID) error { +func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID, updates map[string]any) 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(updates).Error; err != nil { return err } - return nil + + var updatedUser User + if err := tx.Where("user_id = ?", userId).First(&updatedUser).Error; err != nil { + return err + } + + return updatedUser.UpdateSearchIndex(&ctx) }) } diff --git a/service/service_user/get_user_info.go b/service/service_user/get_user_info.go index c60e3d4..05c1011 100644 --- a/service/service_user/get_user_info.go +++ b/service/service_user/get_user_info.go @@ -12,13 +12,13 @@ import ( 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"` + 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"` + AllowPublic *bool `json:"allow_public"` } type UserInfoPayload struct { @@ -80,13 +80,13 @@ func (self *UserServiceImpl) GetUserInfo(payload *UserInfoPayload) (result *User Data: &UserInfoData{ UserId: userData.UserId, Email: userData.Email, - Username: userData.Username, - Nickname: userData.Nickname, - Subtitle: userData.Subtitle, - Avatar: userData.Avatar, - Bio: userData.Bio, + Username: &userData.Username, + Nickname: &userData.Nickname, + Subtitle: &userData.Subtitle, + Avatar: &userData.Avatar, + Bio: &userData.Bio, PermissionLevel: userData.PermissionLevel, - AllowPublic: userData.AllowPublic, + AllowPublic: &userData.AllowPublic, }, } diff --git a/service/service_user/update_user_info.go b/service/service_user/update_user_info.go index 43b4836..4920407 100644 --- a/service/service_user/update_user_info.go +++ b/service/service_user/update_user_info.go @@ -1,6 +1,7 @@ package service_user import ( + "log/slog" "net/url" "nixcn-cms/data" "nixcn-cms/internal/cryptography" @@ -12,15 +13,36 @@ import ( 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) + updates := make(map[string]any) - if payload.Data.Username != "" { - if len(payload.Data.Username) < 5 || len(payload.Data.Username) >= 255 { + if payload.Data.Username != nil { + val := *payload.Data.Username + if val != "" { + 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: shared.CommonResult{ + HttpCode: 400, + Exception: execption, + }, + Data: nil, + } + + return + } + } + updates["username"] = *payload.Data.Username + } + + slog.Debug("DataNickname", slog.Any("DataNickName", payload.Data.Nickname)) + + if payload.Data.Nickname != nil { + if utf8.RuneCountInString(*payload.Data.Nickname) > 24 { execption := new(exception.Builder). SetStatus(exception.StatusUser). SetService(exception.ServiceUser). @@ -37,52 +59,35 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U return } - userData.SetUsername(payload.Data.Username) + updates["nickname"] = *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) + if payload.Data.Subtitle != nil { + 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: shared.CommonResult{ - HttpCode: 400, - Exception: execption, - }, - Data: nil, + result = &UserInfoResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: execption, + }, + Data: nil, + } + + return } - - return + updates["subtitle"] = *payload.Data.Subtitle } - userData.SetNickname(payload.Data.Nickname) - 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: shared.CommonResult{ - HttpCode: 400, - Exception: execption, - }, - Data: nil, - } - - return - } - userData.SetSubtitle(payload.Data.Subtitle) - - if payload.Data.Avatar != "" { - _, err := url.ParseRequestURI(payload.Data.Avatar) + if payload.Data.Avatar != nil { + _, err := url.ParseRequestURI(*payload.Data.Avatar) if err != nil { execption := new(exception.Builder). SetStatus(exception.StatusUser). @@ -103,38 +108,62 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U return } - userData.SetAvatar(payload.Data.Avatar) - } else if payload.Data.Avatar == "" { - userData.SetAvatar("") + updates["avatar"] = *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) + if payload.Data.Bio != nil { + val := *payload.Data.Bio + if val != "" { + 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: shared.CommonResult{ - HttpCode: 400, - Exception: execption, - }, - Data: nil, + result = &UserInfoResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: execption, + }, + Data: nil, + } + + return } - - return } - userData.SetBio(payload.Data.Bio) - } else if payload.Data.Bio == "" { - userData.SetBio("") + updates["bio"] = *payload.Data.Bio } - err = userData.UpdateByUserID(payload.Context, &payload.UserId) + if payload.Data.AllowPublic != nil { + updates["allow_public"] = *payload.Data.AllowPublic + } + + if len(updates) == 0 { + 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: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: nil, + } + + return + } + + err = new(data.User).UpdateByUserID(payload.Context, &payload.UserId, updates) if err != nil { exception := new(exception.Builder). SetStatus(exception.StatusServer). -- 2.49.1 From 3b39141bf0e66a1a3d12b874c4f2858db495bfc5 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 21:28:42 +0800 Subject: [PATCH 037/195] Fix update_user_info avatar logic in service_user Signed-off-by: Asai Neko --- service/service_user/update_user_info.go | 39 +++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/service/service_user/update_user_info.go b/service/service_user/update_user_info.go index 4920407..2edddb4 100644 --- a/service/service_user/update_user_info.go +++ b/service/service_user/update_user_info.go @@ -87,26 +87,29 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U } if payload.Data.Avatar != nil { - _, 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) + val := *payload.Data.Avatar + if val != "" { + _, 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: shared.CommonResult{ - HttpCode: 400, - Exception: execption, - }, - Data: nil, + result = &UserInfoResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: execption, + }, + Data: nil, + } + + return } - - return } updates["avatar"] = *payload.Data.Avatar } -- 2.49.1 From 5da6e9ce2521800d2ee3e4ad8ccf9c9616db2af1 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 29 Jan 2026 22:12:46 +0800 Subject: [PATCH 038/195] Remove Debug output for update_user_info in service_user Signed-off-by: Asai Neko --- service/service_user/update_user_info.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/service/service_user/update_user_info.go b/service/service_user/update_user_info.go index 2edddb4..80fdbd1 100644 --- a/service/service_user/update_user_info.go +++ b/service/service_user/update_user_info.go @@ -1,7 +1,6 @@ package service_user import ( - "log/slog" "net/url" "nixcn-cms/data" "nixcn-cms/internal/cryptography" @@ -39,8 +38,6 @@ func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *U updates["username"] = *payload.Data.Username } - slog.Debug("DataNickname", slog.Any("DataNickName", payload.Data.Nickname)) - if payload.Data.Nickname != nil { if utf8.RuneCountInString(*payload.Data.Nickname) > 24 { execption := new(exception.Builder). -- 2.49.1 From b70095c99ed7933d9c011aa19273246021909472 Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Thu, 29 Jan 2026 22:17:16 +0800 Subject: [PATCH 039/195] feat(client): profile improvements Signed-off-by: Noa Virellia --- client/cms/eslint.config.js | 2 +- client/cms/package.json | 9 +- client/cms/pnpm-lock.yaml | 458 ++++++++++++++++++ .../cms/src/components/checkin/qr-dialog.tsx | 24 +- .../profile/edit-profile-dialog.tsx | 39 +- .../src/components/profile/main-profile.tsx | 29 +- .../cms/src/components/sidebar/nav-user.tsx | 20 +- client/cms/src/components/theme-provider.tsx | 1 - client/cms/src/components/ui/combobox.tsx | 310 ++++++++++++ client/cms/src/components/ui/input-group.tsx | 168 +++++++ client/cms/src/components/ui/switch.tsx | 33 ++ client/cms/src/components/ui/textarea.tsx | 18 + client/cms/src/hooks/data/useExchangeToken.ts | 12 +- client/cms/src/hooks/data/useGetMagicLink.ts | 6 +- client/cms/src/hooks/data/useUpdateUser.ts | 4 +- client/cms/src/hooks/data/useUserInfo.ts | 4 +- client/cms/src/lib/client.ts | 23 +- client/cms/src/lib/token.ts | 7 +- client/cms/src/routes/authorize.tsx | 7 +- client/cms/src/routes/token.tsx | 17 +- client/cms/src/vite.env.d.ts | 6 +- 21 files changed, 1114 insertions(+), 83 deletions(-) create mode 100644 client/cms/src/components/ui/combobox.tsx create mode 100644 client/cms/src/components/ui/input-group.tsx create mode 100644 client/cms/src/components/ui/switch.tsx create mode 100644 client/cms/src/components/ui/textarea.tsx diff --git a/client/cms/eslint.config.js b/client/cms/eslint.config.js index 75e958b..377f8ce 100644 --- a/client/cms/eslint.config.js +++ b/client/cms/eslint.config.js @@ -3,7 +3,7 @@ import pluginQuery from '@tanstack/eslint-plugin-query'; export default antfu({ gitignore: true, - ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*'], + ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*', 'src/client/**/*'], react: true, stylistic: { semi: true, diff --git a/client/cms/package.json b/client/cms/package.json index b01e804..7350502 100644 --- a/client/cms/package.json +++ b/client/cms/package.json @@ -3,6 +3,7 @@ "type": "module", "version": "0.0.0", "private": true, + "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316", "scripts": { "dev": "vite", "build": "tsc -b && vite build", @@ -11,6 +12,10 @@ "gen": "openapi-ts" }, "dependencies": { + "@base-ui/react": "^1.1.0", + "@dicebear/collection": "^9.3.1", + "@dicebear/core": "^9.3.1", + "@dicebear/identicon": "^9.3.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -25,6 +30,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", @@ -98,6 +104,5 @@ }, "lint-staged": { "*": "eslint --fix" - }, - "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316" + } } diff --git a/client/cms/pnpm-lock.yaml b/client/cms/pnpm-lock.yaml index 1f4dcea..412997d 100644 --- a/client/cms/pnpm-lock.yaml +++ b/client/cms/pnpm-lock.yaml @@ -8,6 +8,18 @@ importers: .: dependencies: + '@base-ui/react': + specifier: ^1.1.0 + version: 1.1.0(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dicebear/collection': + specifier: ^9.3.1 + version: 9.3.1(@dicebear/core@9.3.1) + '@dicebear/core': + specifier: ^9.3.1 + version: 9.3.1 + '@dicebear/identicon': + specifier: ^9.3.1 + version: 9.3.1(@dicebear/core@9.3.1) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -50,6 +62,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-tabs': specifier: ^1.1.13 version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -406,12 +421,229 @@ packages: resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.1.0': + resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@base-ui/utils@0.2.4': + resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@dicebear/adventurer-neutral@9.3.1': + resolution: {integrity: sha512-MKrzLkAGx0cdBVD+XJu6ERhdJjWsjoFS+0nF9MZT17h/m/Q12FSoj+ACoKTEXBS/LBQfQqjA9HstBlSxMzmBdw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/adventurer@9.3.1': + resolution: {integrity: sha512-MBCA8QtRC4mWbYncFDxI67LxxXMccsORqJS8osD4F/MgOPMJsdoN9QrRfsY/MjO+4NbTSxsVzOhn2nf1WzoLbA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/avataaars-neutral@9.3.1': + resolution: {integrity: sha512-d9enbUJcRfMui0ZESJ9ofJXKJPdqrzKgqefT9fcC8EfOvP0WqVtsUzcPj9l6FYhG1fMDdTsx+A8e//1lCynbQQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/avataaars@9.3.1': + resolution: {integrity: sha512-gQwtaTfPVwNAvVktdTjyhGnQtt5ifeE/6XyMX/fUJTTo/uI2NLy4LedzjsibA/DW8xi+TbgUyXlyTaJs0H6MGA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/big-ears-neutral@9.3.1': + resolution: {integrity: sha512-Fvw/GoT+3q77zwUbHOujGujQ4oVgtoOXE7ByfxcPeVcaUUTRARpWXlNwUBg0zt+o/Dfv875awpt3sIgKuecGsw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/big-ears@9.3.1': + resolution: {integrity: sha512-JN2ZlrSvpKJNyRAFzyeg+Y5wBG0EZQc8Ds5bZIHkf2/uaLUQIeDT1At2Sr7hSJDKSYZ8z83H6ckbzpDl5b9MzQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/big-smile@9.3.1': + resolution: {integrity: sha512-c5USb4n3Zw32WIJUZqc2+mCe3vbN6XJtZjKtFbisFujMAX6I3avRf6S1JBbm5oT86ynGH6P1/EZ2K7WkThfEBg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/bottts-neutral@9.3.1': + resolution: {integrity: sha512-Ea3dZ7/absDmedpFIZp+yoeS6Dq0sZ8W87xw39SS45Mr1s3i4lVd/0XWc9U5QBl0XrP4CQKB7b2QpcSY4tIYtg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/bottts@9.3.1': + resolution: {integrity: sha512-qIPokserYLIwpScbsvFADwspBfa1Mg8JFEtYcXYcbPLnNek8bZiAhpQSc1bHSqHjm10bFEjvTr0opSNr72CBzw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/collection@9.3.1': + resolution: {integrity: sha512-3hDYu9K4quu9jiXQTno2e0AyBzmrqm1PE6Mw7u2gYOZZ5GsSqrDdNHQODShyzqDF1LuyypZY4XN4YjFJ6fWqig==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/core@9.3.1': + resolution: {integrity: sha512-N6Gl9z3SxYp1OVtOzQegtURFqr0D62l3QcXgvshDAVXDNjkziZ5gWj//JxYJRWldNZfVp9/pm97V3ExKI5AXPg==} + engines: {node: '>=18.0.0'} + + '@dicebear/croodles-neutral@9.3.1': + resolution: {integrity: sha512-NEOV/j+pqxhFmxSC4EFjPgjbTsnOXkX3WgLLVz0PZBpVpS2kPOwBQXZT2fUGZIq7zHucWWatnMaKBvd2LYmFhA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/croodles@9.3.1': + resolution: {integrity: sha512-p40OXll38AYpWh7vOVuapv6ClQuzaMh77e++QJjJGNr6n1OE6YmmQbp6XzE7iELzz2yGoCPIm/FjI+zcH0aAwA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/dylan@9.3.1': + resolution: {integrity: sha512-CWP8S9heivya/KSSF72IJ6QKE5bUsoxKSlnLD21uO+NAm5Mzkw00PM0cgA4RvnPNf0Et7HmoJXrrvOBavWE65A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/fun-emoji@9.3.1': + resolution: {integrity: sha512-oSJPxHvAnORxa3FJYwYGQcUuP5LIFRKzMJJ9RP4D5GTYmpjsdG0K895eo4vKkXrY/BVNVROBlfK0KcTX1xOU8g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/glass@9.3.1': + resolution: {integrity: sha512-yNxDgIE9A+/n5VgOMH8P0qb1EGsMhMSSrl0s6ZnTBpHLGwRv1iGXezJaZrkx/ZSPsp8KlOOfTodeqi2vkVdFHg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/icons@9.3.1': + resolution: {integrity: sha512-p8BrJ/6C2smLKU8vFFu+B54zD/GFbdjumVubzcURjvbOh1YOWU2CD4TruSBZ4d2zbAwgJIAjE+5oANB0a1gfdg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/identicon@9.3.1': + resolution: {integrity: sha512-P3TmN7pRqlS8S9/1E+lGEMrBbQvjjXGNgXnw+Okviq+41172iLVg6Wv0nbNsOyF9QjRTjrJMq4VT3XgOuU4JAg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/initials@9.3.1': + resolution: {integrity: sha512-1O61oYxKVeeGL6QcNCcxH7zsqbp37NmHbR/Y5CVqr6AVv0bBswvCzVzUv/Zmmsp70DoYQB+lbX+oNIdcqUWAaw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/lorelei-neutral@9.3.1': + resolution: {integrity: sha512-GP2EX3w8Di4b3XN0uM7lARbg1iZ9r0zaZHlUbCE2CzFy3xxrKSrRDYf2BvVt7x76doijXR5SLm4DMbEA9ARJWA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/lorelei@9.3.1': + resolution: {integrity: sha512-4btARyv+ITuL3GWKA68/h6hAPL52lN1034JHx+dJCjy7zXrsXvFKkQj62LbCkQKHQOihTkAW1dfccVQ7mlGn4w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/micah@9.3.1': + resolution: {integrity: sha512-LAPY6Zlw/nh0Xts4aIY5d0hlaJEfSah+M5GoBRzKFKlieYdee7hvE8gsCE+OZ4pZUc98Dh7h0XXqVt/ojYW3jw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/miniavs@9.3.1': + resolution: {integrity: sha512-LRLKxDAIk8fW/88YB0vbYiJ850FaO2EcdznOfyW0izDp0ghTGZXsO5B5RUiLTunH8ZCnDdA+DtaugaFTdvOx/Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/notionists-neutral@9.3.1': + resolution: {integrity: sha512-Z9dikJjibAc94EtFnHQb1+ADMISLedgLls5+ARiKwKjPcYZcuRm4U8kR9tMLmqggro10uJlT7YrLSCC/5abUXA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/notionists@9.3.1': + resolution: {integrity: sha512-xSukD2J+iKaKq/kEOZ6svwon9sQYRpgIeNC7Gfskb7uyC+iUHQmCy6hSxLFGIOFVqEbw6Ow8uNpn9NqaFpQA4w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/open-peeps@9.3.1': + resolution: {integrity: sha512-g4A3XcLrKPy44ajlhWfmGXYUDzXfogzB/H7Z46k+mxvrhVSF0jsmReYjX80jqHNeEZ9ikIpR5g4Hbo6vmOmjGQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/personas@9.3.1': + resolution: {integrity: sha512-2xqaiY0/uHKFNhC+ZEBINJZM9/fC8gUMFCqP4N6QuXkFbqNZn4RjgbTITkGtRE5Z4m2q9hEfPey4Dc9jep5lzA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/pixel-art-neutral@9.3.1': + resolution: {integrity: sha512-N3mcC4CFTAMk3TqRvZVsZAGY2NONnQwoGpP+MD4E2GF+kVWoQYpvzOybVgFoOz2G0Oe4HAwSO5Qt7KTbAiD7KA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/pixel-art@9.3.1': + resolution: {integrity: sha512-yUufylvVqkb9wpG/sYRzNTeSk1YbzVgSq/ZSMyxy1kx/R4BhOkiZBSs6Ra3VjeKWVNDBzUWERaVdylLbFvAQaw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/rings@9.3.1': + resolution: {integrity: sha512-1bQTKJbVzpBPbhSyHS5bzlRjYIRQKO1hR0JGmC/ZWFiE9+ySk/NNwkNghvcDxvDUaz02NLSJXlSp2T8nrsdNHg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/shapes@9.3.1': + resolution: {integrity: sha512-xzw/BWSQCznRDFBp8DKQtg1Jxawq+R3upOM2pURwbCPC+9bi8f8CAz1SExA3tlAbbrVx0HQdRKIYS3GW6/GBBA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/thumbs@9.3.1': + resolution: {integrity: sha512-HS14oyT9HXLT8OPqEz8n0Bdob3oRWoNZ5PSZrxT4nyYXxh0rDSxCCOFwPKanXznk1qCAngtAvuzzID3vo7UG3A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/toon-head@9.3.1': + resolution: {integrity: sha512-9a9ydhbrVG57NuscH92yzIMQ0yxEPgJtzOMG1QR6jWctgbeEuzQvJDPvJQTxtfFjx71VlQNsSL40/5rnMtCaTw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -1105,6 +1337,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tabs@1.1.13': resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: @@ -3793,6 +4038,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + reserved-identifiers@1.2.0: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} @@ -3960,6 +4208,9 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -4470,6 +4721,31 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@base-ui/react@1.1.0(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.6 + '@base-ui/utils': 0.2.4(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/utils': 0.2.10 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + reselect: 5.1.1 + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + + '@base-ui/utils@0.2.4(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.28.6 + '@floating-ui/utils': 0.2.10 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -4481,6 +4757,169 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@dicebear/adventurer-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/adventurer@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/avataaars-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/avataaars@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/big-ears-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/big-ears@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/big-smile@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/bottts-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/bottts@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/collection@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/adventurer': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/adventurer-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/avataaars': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/avataaars-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/big-ears': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/big-ears-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/big-smile': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/bottts': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/bottts-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/core': 9.3.1 + '@dicebear/croodles': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/croodles-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/dylan': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/fun-emoji': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/glass': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/icons': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/identicon': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/initials': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/lorelei': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/lorelei-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/micah': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/miniavs': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/notionists': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/notionists-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/open-peeps': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/personas': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/pixel-art': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/pixel-art-neutral': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/rings': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/shapes': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/thumbs': 9.3.1(@dicebear/core@9.3.1) + '@dicebear/toon-head': 9.3.1(@dicebear/core@9.3.1) + + '@dicebear/core@9.3.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@dicebear/croodles-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/croodles@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/dylan@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/fun-emoji@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/glass@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/icons@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/identicon@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/initials@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/lorelei-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/lorelei@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/micah@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/miniavs@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/notionists-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/notionists@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/open-peeps@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/personas@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/pixel-art-neutral@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/pixel-art@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/rings@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/shapes@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/thumbs@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + + '@dicebear/toon-head@9.3.1(@dicebear/core@9.3.1)': + dependencies: + '@dicebear/core': 9.3.1 + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': dependencies: react: 19.2.3 @@ -5166,6 +5605,21 @@ snapshots: optionalDependencies: '@types/react': 19.2.8 + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -8263,6 +8717,8 @@ snapshots: require-main-filename@2.0.0: {} + reselect@5.1.1: {} + reserved-identifiers@1.2.0: {} resolve-from@4.0.0: {} @@ -8429,6 +8885,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tabbable@6.4.0: {} + tagged-tag@1.0.0: {} tailwind-merge@3.4.0: {} diff --git a/client/cms/src/components/checkin/qr-dialog.tsx b/client/cms/src/components/checkin/qr-dialog.tsx index a1fa442..df2a81c 100644 --- a/client/cms/src/components/checkin/qr-dialog.tsx +++ b/client/cms/src/components/checkin/qr-dialog.tsx @@ -38,20 +38,20 @@ function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) const data = { data: { checkin_code: `dummy${eventId}${enabled}` } }; return data ? ( - <> -
- -
- -
- {data.data.checkin_code} + <> +
+
- - - ) + +
+ {data.data.checkin_code} +
+
+ + ) : ( - - ); + + ); } function QrSectionSkeleton() { diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile-dialog.tsx index c98d4c7..47be953 100644 --- a/client/cms/src/components/profile/edit-profile-dialog.tsx +++ b/client/cms/src/components/profile/edit-profile-dialog.tsx @@ -1,4 +1,5 @@ import { useForm } from '@tanstack/react-form'; +import { useState } from 'react'; import { toast } from 'sonner'; import z from 'zod'; import { Button } from '@/components/ui/button'; @@ -21,12 +22,14 @@ import { } from '@/components/ui/input'; import { useUpdateUser } from '@/hooks/data/useUpdateUser'; import { useUserInfo } from '@/hooks/data/useUserInfo'; +import { Switch } from '../ui/switch'; const formSchema = z.object({ username: z.string().min(5), - nickname: z.string().min(1), - subtitle: z.string().min(1), - avatar: z.url().min(1), + nickname: z.string(), + subtitle: z.string(), + avatar: z.url().or(z.literal('')), + allow_public: z.boolean(), }); export function EditProfileDialog() { const { data } = useUserInfo(); @@ -39,6 +42,7 @@ export function EditProfileDialog() { username: user.username, nickname: user.nickname, subtitle: user.subtitle, + allow_public: user.allow_public, }, validators: { onBlur: formSchema, @@ -57,8 +61,16 @@ export function EditProfileDialog() { }, }); + const [open, setOpen] = useState(false); + + if (!open) { + setTimeout(() => { + form.reset(); + }, 200); + } + return ( - + @@ -67,7 +79,7 @@ export function EditProfileDialog() { onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); - void form.handleSubmit(); + form.handleSubmit().then(() => setOpen(false)); }} className="grid gap-4" > @@ -138,13 +150,24 @@ export function EditProfileDialog() { )} + + {field => ( + + 公开个人资料 + field.handleChange(e)} defaultChecked={user.allow_public} /> + + )} + - - - + [state.canSubmit]} + children={([canSubmit]) => ( + + )} + /> diff --git a/client/cms/src/components/profile/main-profile.tsx b/client/cms/src/components/profile/main-profile.tsx index 425d632..28d4adf 100644 --- a/client/cms/src/components/profile/main-profile.tsx +++ b/client/cms/src/components/profile/main-profile.tsx @@ -1,10 +1,12 @@ +import { identicon } from '@dicebear/collection'; +import { createAvatar } from '@dicebear/core'; import MDEditor from '@uiw/react-md-editor'; import { isNil } from 'lodash-es'; import { Mail, Pencil } from 'lucide-react'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import Markdown from 'react-markdown'; import { toast } from 'sonner'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Avatar, AvatarImage } from '@/components/ui/avatar'; import { useUpdateUser } from '@/hooks/data/useUpdateUser'; import { useUserInfo } from '@/hooks/data/useUserInfo'; import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils'; @@ -18,6 +20,14 @@ export function MainProfile() { const [enableBioEdit, setEnableBioEdit] = useState(false); const { mutateAsync } = useUpdateUser(); + const IdentIcon = useMemo(() => { + const avatar = createAvatar(identicon, { + size: 128, + seed: user.user_id, + }).toDataUri(); + return Avatar; + }, [user.user_id]); + return (
@@ -25,8 +35,7 @@ export function MainProfile() {
- - CN + {user.avatar ? : IdentIcon}
@@ -45,12 +54,12 @@ export function MainProfile() { {/* Bio */} {enableBioEdit ? ( - - ) + + ) :
{bio}
}