diff --git a/.gitea/workflow/check.yaml b/.gitea/workflow/check.yaml
deleted file mode 100644
index 2b78c23..0000000
--- a/.gitea/workflow/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 --frozen-lockfile
-
- - 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 ./...
diff --git a/.gitignore b/.gitignore
index e9e6933..573623d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,6 @@ __MACOSX
# go gen
*_gen.go
+
+# test files
+.test/
diff --git a/Containerfile b/Containerfile
index 795094f..e40aa00 100644
--- a/Containerfile
+++ b/Containerfile
@@ -1,25 +1,13 @@
-FROM docker.io/node:22-alpine AS client-cms-build
-RUN apk add just -y
-RUN npm install -g corepack && \
- corepack enable
-WORKDIR /app
-ENV VITE_APP_BASE_URL=$CLIENT_BASE_URL
-COPY . .
-RUN just build-client-cms
-
-FROM docker.io/busybox:1.37 AS client-cms
-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 install github.com/swaggo/swag/cmd/swag@latest
RUN go mod tidy && \
+ go generate . && \
+ go test ./... && \
go build -o /app/nixcn-cms
-FROM docker.io/alpine:3.23 AS backend
+FROM docker.io/alpine:3.23
WORKDIR /app
COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms
EXPOSE 8000
diff --git a/api/agenda/agenda_handler_test.go b/api/agenda/agenda_handler_test.go
new file mode 100644
index 0000000..5ef4d49
--- /dev/null
+++ b/api/agenda/agenda_handler_test.go
@@ -0,0 +1,443 @@
+package agenda
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+func issueToken(t *testing.T, userId uuid.UUID) string {
+ t.Helper()
+ tok := &authtoken.Token{Application: viper.GetString("server.application")}
+ access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId)
+ require.NoError(t, err)
+ return access
+}
+
+func newAgendaRouter(t *testing.T) *gin.Engine {
+ t.Helper()
+ r := gin.New()
+ ApiHandler(r.Group("/agenda"))
+ return r
+}
+
+func postWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func patchWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func getWithBearer(t *testing.T, r *gin.Engine, path, token string) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+// seedEventWithAttendee creates an event owned by owner and joins it with attendee.
+func seedEventWithAttendee(t *testing.T, owner, attendee *data.User) *data.Event {
+ t.Helper()
+ ctx := t.Context()
+
+ eventSvc := service_event.NewEventService()
+ cr := eventSvc.Create(&service_event.EventCreatePayload{
+ Context: ctx,
+ Data: &service_event.EventCreateData{
+ UserId: owner.UserId.String(),
+ PermissionLevel: owner.PermissionLevel,
+ Type: "party",
+ Name: "Agenda Test Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 100,
+ },
+ })
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ eventId, err := uuid.Parse(cr.Data.EventId)
+ require.NoError(t, err)
+
+ // Join as attendee
+ agendaSvc := service_agenda.NewAgendaService()
+ jr := agendaSvc.Submit(&service_agenda.SubmitPayload{
+ Context: ctx,
+ UserId: attendee.UserId,
+ Data: &service_agenda.SubmitData{EventId: eventId, Name: "dummy"},
+ })
+ // It's OK if this fails due to not being joined; we just need the event.
+ _ = jr
+
+ // Actually join via event service
+ joinResult := eventSvc.Join(&service_event.EventJoinPayload{
+ Context: ctx,
+ Data: &service_event.EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: attendee.UserId.String(),
+ },
+ })
+ require.Equal(t, 200, joinResult.Common.HttpCode)
+
+ var event data.Event
+ require.NoError(t, data.Database.Where("event_id = ?", eventId).First(&event).Error)
+ return &event
+}
+
+// ---- Submit ----
+
+func TestAgendaSubmitHandlerNoAuth(t *testing.T) {
+ testutil.Setup(t)
+ r := newAgendaRouter(t)
+
+ w := postWithBearer(t, r, "/agenda/submit", "", map[string]any{
+ "event_id": uuid.New().String(),
+ "name": "Test Talk",
+ })
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
+
+func TestAgendaSubmitHandlerMissingFields(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ // Missing name and description → handler returns 400
+ w := postWithBearer(t, r, "/agenda/submit", token, map[string]any{
+ "event_id": uuid.New().String(),
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaSubmitHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ token := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ w := postWithBearer(t, r, "/agenda/submit", token, service_agenda.SubmitData{
+ EventId: event.EventId,
+ Name: "My Talk",
+ Description: "About Go testing",
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- ScheduleGet ----
+
+func TestAgendaScheduleGetHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newAgendaRouter(t)
+
+ w := getWithBearer(t, r, "/agenda/schedule?event_id=not-a-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaScheduleGetHandlerNotPublished(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ token := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ w := getWithBearer(t, r, "/agenda/schedule?event_id="+event.EventId.String(), token)
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
+
+// ---- MyList ----
+
+func TestAgendaMyListHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newAgendaRouter(t)
+
+ w := getWithBearer(t, r, "/agenda/my-list?event_id=bad-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaMyListHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ token := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ w := getWithBearer(t, r, "/agenda/my-list?event_id="+event.EventId.String(), token)
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- List (manager) ----
+
+func TestAgendaListHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, manager.UserId)
+ r := newAgendaRouter(t)
+
+ w := getWithBearer(t, r, "/agenda/list?event_id=bad-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Review (manager) ----
+
+func TestAgendaReviewHandlerMissingFields(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, manager.UserId)
+ r := newAgendaRouter(t)
+
+ w := patchWithBearer(t, r, "/agenda/review", token, map[string]any{
+ "status": "approved",
+ // missing agenda_id and event_id → 400
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaReviewHandlerInvalidStatus(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, manager.UserId)
+ r := newAgendaRouter(t)
+
+ w := patchWithBearer(t, r, "/agenda/review", token, service_agenda.AgendaReviewData{
+ EventId: uuid.New(),
+ AgendaId: uuid.New(),
+ Status: "maybe",
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaReviewHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ managerToken := issueToken(t, manager.UserId)
+ attendeeToken := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ submitW := postWithBearer(t, r, "/agenda/submit", attendeeToken, service_agenda.SubmitData{
+ EventId: event.EventId,
+ Name: "Talk to Review",
+ Description: "desc",
+ })
+ require.Equal(t, http.StatusOK, submitW.Code)
+ var submitResp map[string]any
+ require.NoError(t, json.Unmarshal(submitW.Body.Bytes(), &submitResp))
+ agendaId := submitResp["data"].(map[string]any)["agenda_id"].(string)
+
+ w := patchWithBearer(t, r, "/agenda/review", managerToken, service_agenda.AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: uuid.MustParse(agendaId),
+ Status: "approved",
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- List (success) ----
+
+func TestAgendaListHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ managerToken := issueToken(t, manager.UserId)
+ attendeeToken := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ postWithBearer(t, r, "/agenda/submit", attendeeToken, service_agenda.SubmitData{
+ EventId: event.EventId,
+ Name: "Talk 1",
+ })
+
+ w := getWithBearer(t, r, "/agenda/list?event_id="+event.EventId.String(), managerToken)
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- Update ----
+
+func TestAgendaUpdateHandlerMissingAgendaId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newAgendaRouter(t)
+
+ w := patchWithBearer(t, r, "/agenda/update", token, map[string]any{
+ "name": "New Name",
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaUpdateHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ attendeeToken := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ submitW := postWithBearer(t, r, "/agenda/submit", attendeeToken, service_agenda.SubmitData{
+ EventId: event.EventId,
+ Name: "Original Talk",
+ Description: "desc",
+ })
+ require.Equal(t, http.StatusOK, submitW.Code)
+ var submitResp map[string]any
+ require.NoError(t, json.Unmarshal(submitW.Body.Bytes(), &submitResp))
+ agendaId := submitResp["data"].(map[string]any)["agenda_id"].(string)
+
+ newName := "Updated Talk"
+ w := patchWithBearer(t, r, "/agenda/update", attendeeToken, service_agenda.AgendaUpdateData{
+ AgendaId: uuid.MustParse(agendaId),
+ Name: &newName,
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- Schedule PATCH ----
+
+func TestAgendaScheduleHandlerMissingAgendaId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, manager.UserId)
+ r := newAgendaRouter(t)
+
+ w := patchWithBearer(t, r, "/agenda/schedule", token, map[string]any{
+ "start_time": time.Now().Add(time.Hour),
+ "end_time": time.Now().Add(2 * time.Hour),
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestAgendaScheduleHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ managerToken := issueToken(t, manager.UserId)
+ attendeeToken := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ submitW := postWithBearer(t, r, "/agenda/submit", attendeeToken, service_agenda.SubmitData{
+ EventId: event.EventId,
+ Name: "Schedule Me",
+ Description: "base64desc",
+ })
+ require.Equal(t, http.StatusOK, submitW.Code)
+ var submitResp map[string]any
+ require.NoError(t, json.Unmarshal(submitW.Body.Bytes(), &submitResp))
+ agendaId := submitResp["data"].(map[string]any)["agenda_id"].(string)
+
+ reviewW := patchWithBearer(t, r, "/agenda/review", managerToken, service_agenda.AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: uuid.MustParse(agendaId),
+ Status: "approved",
+ })
+ require.Equal(t, http.StatusOK, reviewW.Code)
+
+ now := time.Now()
+ w := patchWithBearer(t, r, "/agenda/schedule", managerToken, service_agenda.AgendaScheduleData{
+ AgendaId: uuid.MustParse(agendaId),
+ StartTime: now.Add(time.Hour),
+ EndTime: now.Add(2 * time.Hour),
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- ScheduleGet (success) ----
+
+func TestAgendaScheduleGetHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ attendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ event := seedEventWithAttendee(t, owner, attendee)
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ managerToken := issueToken(t, manager.UserId)
+ attendeeToken := issueToken(t, attendee.UserId)
+ r := newAgendaRouter(t)
+
+ submitW := postWithBearer(t, r, "/agenda/submit", attendeeToken, service_agenda.SubmitData{
+ EventId: event.EventId,
+ Name: "Published Talk",
+ Description: "base64desc",
+ })
+ require.Equal(t, http.StatusOK, submitW.Code)
+ var submitResp map[string]any
+ require.NoError(t, json.Unmarshal(submitW.Body.Bytes(), &submitResp))
+ agendaId := submitResp["data"].(map[string]any)["agenda_id"].(string)
+
+ patchWithBearer(t, r, "/agenda/review", managerToken, service_agenda.AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: uuid.MustParse(agendaId),
+ Status: "approved",
+ })
+
+ now := time.Now()
+ patchWithBearer(t, r, "/agenda/schedule", managerToken, service_agenda.AgendaScheduleData{
+ AgendaId: uuid.MustParse(agendaId),
+ StartTime: now.Add(time.Hour),
+ EndTime: now.Add(2 * time.Hour),
+ })
+
+ require.NoError(t, new(data.Event).PatchByEventId(t.Context(), event.EventId, data.WithIsAgendaPublished(true)))
+
+ w := getWithBearer(t, r, "/agenda/schedule?event_id="+event.EventId.String(), attendeeToken)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ items, ok := resp["data"].([]any)
+ require.True(t, ok)
+ assert.Len(t, items, 1)
+}
diff --git a/api/agenda/handler.go b/api/agenda/handler.go
new file mode 100644
index 0000000..95bbad7
--- /dev/null
+++ b/api/agenda/handler.go
@@ -0,0 +1,32 @@
+package agenda
+
+import (
+ "nixcn-cms/middleware"
+ "nixcn-cms/service/service_agenda"
+
+ "github.com/gin-gonic/gin"
+)
+
+type AgendaHandler struct {
+ svc service_agenda.AgendaService
+}
+
+func ApiHandler(r *gin.RouterGroup) {
+ agendaSvc := service_agenda.NewAgendaService()
+ agendaHandler := &AgendaHandler{agendaSvc}
+
+ // Lv10+ attendee routes
+ attendee := r.Group("")
+ attendee.Use(middleware.JWTAuth(), middleware.Permission(10))
+ attendee.POST("/submit", agendaHandler.Submit)
+ attendee.PATCH("/update", agendaHandler.Update)
+ attendee.GET("/my-list", agendaHandler.MyList)
+ attendee.GET("/schedule", agendaHandler.ScheduleGet)
+
+ // Manager routes (Lv30+)
+ manager := r.Group("")
+ manager.Use(middleware.JWTAuth(), middleware.Permission(30))
+ manager.PATCH("/review", agendaHandler.Review)
+ manager.PATCH("/schedule", agendaHandler.Schedule)
+ manager.GET("/list", agendaHandler.List)
+}
diff --git a/api/agenda/list.go b/api/agenda/list.go
new file mode 100644
index 0000000..37779a3
--- /dev/null
+++ b/api/agenda/list.go
@@ -0,0 +1,106 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// List retrieves all agenda items for an event. Manager only.
+//
+// @Summary List All Agendas
+// @Description Returns all agendas for the specified event, regardless of status. Manager only.
+// @Tags Agenda
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param event_id query string true "Event ID"
+// @Success 200 {object} utils.RespStatus{data=[]service_agenda.AgendaListItem}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/list [get]
+func (self *AgendaHandler) List(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaList)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ type ListQuery struct {
+ EventId string `form:"event_id"`
+ }
+
+ var query ListQuery
+ if err := c.ShouldBindQuery(&query); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ eventId, err := uuid.Parse(query.EventId)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("invalid event_id")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.List(&service_agenda.AgendaListPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &service_agenda.AgendaListData{
+ EventId: eventId,
+ },
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/agenda/my_list.go b/api/agenda/my_list.go
new file mode 100644
index 0000000..20c4ca0
--- /dev/null
+++ b/api/agenda/my_list.go
@@ -0,0 +1,105 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// MyList retrieves the current user's own agenda submissions for an event.
+//
+// @Summary My Agenda List
+// @Description Returns the calling user's agenda submissions for the specified event. User must be a joined attendee (Lv10+).
+// @Tags Agenda
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param event_id query string true "Event ID"
+// @Success 200 {object} utils.RespStatus{data=[]data.Agenda}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Not an Attendee"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/my-list [get]
+func (self *AgendaHandler) MyList(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "my_list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaMyList)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ type MyListQuery struct {
+ EventId string `form:"event_id"`
+ }
+
+ var query MyListQuery
+ if err := c.ShouldBindQuery(&query); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ eventId, err := uuid.Parse(query.EventId)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("invalid event_id")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.MyList(&service_agenda.AgendaMyListPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &service_agenda.AgendaMyListData{
+ EventId: eventId,
+ },
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/agenda/review.go b/api/agenda/review.go
new file mode 100644
index 0000000..9118a9c
--- /dev/null
+++ b/api/agenda/review.go
@@ -0,0 +1,110 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Review handles manager approval or rejection of an agenda item.
+//
+// @Summary Review Agenda
+// @Description Manager sets the status of an agenda to approved or rejected. Not allowed after agenda is published.
+// @Tags Agenda
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param body body service_agenda.AgendaReviewData true "Review Data"
+// @Success 200 {object} utils.RespStatus{data=nil}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Already Published"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event or Agenda Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/review [patch]
+func (self *AgendaHandler) Review(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "review",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaReview)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ data := new(service_agenda.AgendaReviewData)
+ if err := c.ShouldBindJSON(data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ if data.AgendaId == uuid.Nil || data.EventId == uuid.Nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("agenda_id and event_id are required")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ if data.Status != "approved" && data.Status != "rejected" {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("status must be 'approved' or 'rejected'")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Review(&service_agenda.AgendaReviewPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: data,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/agenda/schedule.go b/api/agenda/schedule.go
new file mode 100644
index 0000000..15caa20
--- /dev/null
+++ b/api/agenda/schedule.go
@@ -0,0 +1,98 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Schedule handles setting start/end times for an approved agenda item.
+//
+// @Summary Schedule Agenda
+// @Description Manager sets start_time and end_time on an approved agenda item. Available even after publish.
+// @Tags Agenda
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param body body service_agenda.AgendaScheduleData true "Schedule Data"
+// @Success 200 {object} utils.RespStatus{data=nil}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Not Approved"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Agenda Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/schedule [patch]
+func (self *AgendaHandler) Schedule(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "schedule",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaSchedule)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ data := new(service_agenda.AgendaScheduleData)
+ if err := c.ShouldBindJSON(data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ if data.AgendaId == uuid.Nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("agenda_id is required")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Schedule(&service_agenda.AgendaSchedulePayload{
+ Context: ctx,
+ UserId: userId,
+ Data: data,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/agenda/schedule_get.go b/api/agenda/schedule_get.go
new file mode 100644
index 0000000..fc1b9ec
--- /dev/null
+++ b/api/agenda/schedule_get.go
@@ -0,0 +1,80 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// ScheduleGet returns the published, scheduled agenda for an event.
+//
+// @Summary Get Agenda Schedule
+// @Description Returns all approved and scheduled agenda items, sorted by start_time ascending. Returns 403 if the agenda has not been published.
+// @Tags Agenda
+// @Produce json
+// @Security Bearer
+// @Param event_id query string true "Event ID"
+// @Success 200 {object} utils.RespStatus{data=[]data.AgendaDoc}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Agenda Not Published"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/schedule [get]
+func (self *AgendaHandler) ScheduleGet(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "schedule_get",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaScheduleGet)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ type ScheduleQuery struct {
+ EventId string `form:"event_id"`
+ }
+
+ var query ScheduleQuery
+ if err := c.ShouldBindQuery(&query); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ eventId, err := uuid.Parse(query.EventId)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("invalid event_id")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.ScheduleGet(&service_agenda.AgendaScheduleGetPayload{
+ Context: ctx,
+ Data: &service_agenda.AgendaScheduleGetData{
+ EventId: eventId,
+ },
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/agenda/submit.go b/api/agenda/submit.go
new file mode 100644
index 0000000..6da648e
--- /dev/null
+++ b/api/agenda/submit.go
@@ -0,0 +1,100 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Submit handles the submission of a new agenda item.
+//
+// @Summary Submit Agenda
+// @Description Creates a new agenda item for a specific attendance record.
+// @Tags Agenda
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param body body service_agenda.SubmitData true "Agenda Submission Data"
+// @Success 200 {object} utils.RespStatus{data=service_agenda.SubmitResponse}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/submit [post]
+func (self *AgendaHandler) Submit(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "submit",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaSubmit)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ data := new(service_agenda.SubmitData)
+
+ if err := c.ShouldBindJSON(data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ if data.EventId == uuid.Nil || data.Name == "" || data.Description == "" {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Invalid Input")),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Submit(&service_agenda.SubmitPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: data,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), &result.Data)
+}
diff --git a/api/agenda/update.go b/api/agenda/update.go
new file mode 100644
index 0000000..8619e27
--- /dev/null
+++ b/api/agenda/update.go
@@ -0,0 +1,113 @@
+package agenda
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_agenda"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Update handles editing an agenda item's name or description.
+//
+// @Summary Update Agenda
+// @Description Submitter may edit their own pending agendas before the event deadline. Managers may edit any agenda with no restrictions.
+// @Tags Agenda
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param body body service_agenda.AgendaUpdateData true "Agenda Update Data"
+// @Success 200 {object} utils.RespStatus{data=nil}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Not Pending / Deadline Passed"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Not Submitter"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Agenda Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /agenda/update [patch]
+func (self *AgendaHandler) Update(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_agenda",
+ "update",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAgendaUpdate)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ permissionLevelOrig, ok := c.Get("permission_level")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Missing PermissionLevel")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ data := new(service_agenda.AgendaUpdateData)
+ if err := c.ShouldBindJSON(data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ if data.AgendaId == uuid.Nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("agenda_id is required")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ data.PermissionLevel = permissionLevelOrig.(uint)
+
+ result := self.svc.Update(&service_agenda.AgendaUpdatePayload{
+ Context: ctx,
+ UserId: userId,
+ Data: data,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/auth/auth_handler_test.go b/api/auth/auth_handler_test.go
new file mode 100644
index 0000000..91738f7
--- /dev/null
+++ b/api/auth/auth_handler_test.go
@@ -0,0 +1,251 @@
+package auth
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/service/service_auth"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+func setupAuthRouter(t *testing.T) *gin.Engine {
+ t.Helper()
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+
+ r := gin.New()
+ ApiHandler(r.Group("/auth"))
+ return r
+}
+
+func postJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func getRequest(t *testing.T, r *gin.Engine, path string) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func postWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ var bodyReader io.Reader
+ if body != nil {
+ b, _ := json.Marshal(body)
+ bodyReader = bytes.NewBuffer(b)
+ }
+ req := httptest.NewRequest(http.MethodPost, path, bodyReader)
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func extractQueryParam(t *testing.T, rawURL, param string) string {
+ t.Helper()
+ u, err := url.Parse(rawURL)
+ require.NoError(t, err)
+ return u.Query().Get(param)
+}
+
+// ---- Magic ----
+
+func TestMagicHandlerSuccess(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ w := postJSON(t, r, "/auth/magic", service_auth.MagicData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Email: "handler@example.com",
+ })
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ data, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.NotEmpty(t, data["uri"])
+}
+
+func TestMagicHandlerInvalidJSON(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ req := httptest.NewRequest(http.MethodPost, "/auth/magic", bytes.NewBufferString("{invalid"))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Redirect ----
+
+func TestRedirectHandlerMissingParams(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ w := getRequest(t, r, "/auth/redirect")
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestRedirectHandlerInvalidCode(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ w := getRequest(t, r, "/auth/redirect?client_id="+testutil.TestClientID+
+ "&redirect_uri=http://localhost/callback&code=bad-code&state=s")
+
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
+
+// ---- Token ----
+
+func TestTokenHandlerInvalidCode(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ w := postJSON(t, r, "/auth/token", service_auth.TokenData{Code: "invalid"})
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
+
+// ---- Refresh ----
+
+func TestRefreshHandlerInvalidToken(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ w := postJSON(t, r, "/auth/refresh", service_auth.RefreshData{RefreshToken: "bad"})
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
+
+// ---- Full flow: Magic → Redirect → Token → Refresh ----
+
+func TestAuthFullFlow(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ // 1. Magic
+ magicW := postJSON(t, r, "/auth/magic", service_auth.MagicData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Email: "flow@example.com",
+ })
+ require.Equal(t, http.StatusOK, magicW.Code)
+
+ var magicResp map[string]any
+ require.NoError(t, json.Unmarshal(magicW.Body.Bytes(), &magicResp))
+ dataMap := magicResp["data"].(map[string]any)
+ rawURI := dataMap["uri"].(string)
+ code := extractQueryParam(t, rawURI, "code")
+ require.NotEmpty(t, code)
+
+ // 2. Redirect → 302
+ redirectW := getRequest(t, r, "/auth/redirect?client_id="+testutil.TestClientID+
+ "&redirect_uri=http://localhost/callback&code="+code+"&state=s")
+ require.Equal(t, http.StatusFound, redirectW.Code)
+
+ location := redirectW.Header().Get("Location")
+ require.NotEmpty(t, location)
+ tokenCode := extractQueryParam(t, location, "code")
+ require.NotEmpty(t, tokenCode)
+
+ // 3. Token
+ tokenW := postJSON(t, r, "/auth/token", service_auth.TokenData{Code: tokenCode})
+ require.Equal(t, http.StatusOK, tokenW.Code)
+
+ var tokenResp map[string]any
+ require.NoError(t, json.Unmarshal(tokenW.Body.Bytes(), &tokenResp))
+ tokenData := tokenResp["data"].(map[string]any)
+ accessToken := tokenData["access_token"].(string)
+ refreshToken := tokenData["refresh_token"].(string)
+
+ // 4. Refresh
+ refreshW := postJSON(t, r, "/auth/refresh", service_auth.RefreshData{
+ RefreshToken: refreshToken,
+ })
+ require.Equal(t, http.StatusOK, refreshW.Code)
+
+ var refreshResp map[string]any
+ require.NoError(t, json.Unmarshal(refreshW.Body.Bytes(), &refreshResp))
+ refreshData := refreshResp["data"].(map[string]any)
+ assert.NotEmpty(t, refreshData["access_token"])
+ assert.NotEqual(t, refreshToken, refreshData["refresh_token"])
+ _ = accessToken
+}
+
+// ---- Exchange ----
+
+func TestExchangeHandlerNoAuth(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ w := postJSON(t, r, "/auth/exchange", service_auth.ExchangeData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ })
+ // No auth header → 401
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
+
+func TestExchangeHandlerSuccess(t *testing.T) {
+ r := setupAuthRouter(t)
+
+ // Step 1: Magic (debug mode, returns redirect URI with code)
+ magicW := postJSON(t, r, "/auth/magic", service_auth.MagicData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Email: "exchange@example.com",
+ })
+ require.Equal(t, http.StatusOK, magicW.Code)
+ var magicResp map[string]any
+ require.NoError(t, json.Unmarshal(magicW.Body.Bytes(), &magicResp))
+ rawURI := magicResp["data"].(map[string]any)["uri"].(string)
+ code := extractQueryParam(t, rawURI, "code")
+
+ // Step 2: Redirect → produces token code
+ redirectW := getRequest(t, r, "/auth/redirect?client_id="+testutil.TestClientID+
+ "&redirect_uri=http://localhost/callback&code="+code+"&state=s")
+ require.Equal(t, http.StatusFound, redirectW.Code)
+ location := redirectW.Header().Get("Location")
+ tokenCode := extractQueryParam(t, location, "code")
+
+ // Step 3: Token → get access token
+ tokenW := postJSON(t, r, "/auth/token", service_auth.TokenData{Code: tokenCode})
+ require.Equal(t, http.StatusOK, tokenW.Code)
+ var tokenResp map[string]any
+ require.NoError(t, json.Unmarshal(tokenW.Body.Bytes(), &tokenResp))
+ accessToken := tokenResp["data"].(map[string]any)["access_token"].(string)
+
+ // Step 4: Exchange (requires JWT Bearer token)
+ w := postWithBearer(t, r, "/auth/exchange", accessToken, service_auth.ExchangeData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+ var exchangeResp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &exchangeResp))
+ exchangeData, ok := exchangeResp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.NotEmpty(t, exchangeData["redirect_uri"])
+}
diff --git a/api/auth/exchange.go b/api/auth/exchange.go
new file mode 100644
index 0000000..73914cd
--- /dev/null
+++ b/api/auth/exchange.go
@@ -0,0 +1,91 @@
+package auth
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_auth"
+ "nixcn-cms/tracer"
+ "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
+// @Security Bearer
+// @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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_auth",
+ "exchange",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAuthExchange)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ var exchangeData service_auth.ExchangeData
+
+ if err := c.ShouldBindJSON(&exchangeData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUnauthorized),
+ exception.WithError(errors.New("Unauthorized")),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 401, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ result := self.svc.Exchange(&service_auth.ExchangePayload{
+ Context: ctx,
+ 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..149552d 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", authHandler.Magic)
+ r.POST("/token", authHandler.Token)
+ r.POST("/refresh", authHandler.Refresh)
+ r.POST("/exchange", middleware.JWTAuth(), authHandler.Exchange)
}
diff --git a/api/auth/magic.go b/api/auth/magic.go
new file mode 100644
index 0000000..00487ff
--- /dev/null
+++ b/api/auth/magic.go
@@ -0,0 +1,63 @@
+package auth
+
+import (
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_auth"
+ "nixcn-cms/tracer"
+ "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} 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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_auth",
+ "magic",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAuthMagic)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ var magicData service_auth.MagicData
+
+ if err := c.ShouldBindJSON(&magicData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ magicData.ClientIP = c.ClientIP()
+
+ result := self.svc.Magic(&service_auth.MagicPayload{
+ Context: ctx,
+ 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
new file mode 100644
index 0000000..2a397ec
--- /dev/null
+++ b/api/auth/redirect.go
@@ -0,0 +1,70 @@
+package auth
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_auth"
+ "nixcn-cms/tracer"
+ "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 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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_auth",
+ "redirect",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAuthRedirect)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ 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 := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Invalid Input")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Redirect(&service_auth.RedirectPayload{
+ Context: ctx,
+ 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
new file mode 100644
index 0000000..3a9c6e8
--- /dev/null
+++ b/api/auth/refresh.go
@@ -0,0 +1,61 @@
+package auth
+
+import (
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_auth"
+ "nixcn-cms/tracer"
+ "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} 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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_auth",
+ "refresh",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAuthRefresh)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ var refreshData service_auth.RefreshData
+
+ if err := c.ShouldBindJSON(&refreshData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Refresh(&service_auth.RefreshPayload{
+ Context: ctx,
+ 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
new file mode 100644
index 0000000..48f7de1
--- /dev/null
+++ b/api/auth/token.go
@@ -0,0 +1,61 @@
+package auth
+
+import (
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_auth"
+ "nixcn-cms/tracer"
+ "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} 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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_auth",
+ "token",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointAuthToken)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ var tokenData service_auth.TokenData
+
+ if err := c.ShouldBindJSON(&tokenData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Token(&service_auth.TokenPayload{
+ Context: ctx,
+ 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/attendance.go b/api/event/attendance.go
new file mode 100644
index 0000000..49ab3ff
--- /dev/null
+++ b/api/event/attendance.go
@@ -0,0 +1,110 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// AttendanceList handles the retrieval of the paginated attendance list for a specific event.
+//
+// @Summary Get Attendance List
+// @Description Retrieves the paginated list of attendees with optional filters. Only accessible by the event owner (Manager). Supports name substring search and KYC status filtering.
+// @Tags Event
+// @Produce json
+// @Security Bearer
+// @Param event_id query string true "Event UUID"
+// @Param name query string false "Substring filter on attendee nickname"
+// @Param kyc_status query string false "KYC filter: 'with_kyc' or 'without_kyc'"
+// @Param limit query int false "Maximum number of results to return (default 20)"
+// @Param offset query int false "Number of results to skip (default 0)"
+// @Param sort_by query string false "Sort field: 'checkin_at' (default) or 'id'"
+// @Param sort_order query string false "Sort direction: 'asc' or 'desc' (default)"
+// @Success 200 {object} utils.RespStatus{data=[]service_event.AttendanceListResponse} "Successful retrieval"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Not Event Owner"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /event/attendance [get]
+func (self *EventHandler) Attendance(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "attendance",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventAttendance)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ eventIdStr := c.Query("event_id")
+ eventId, err := uuid.Parse(eventIdStr)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ limit := c.Query("limit")
+ offset := c.Query("offset")
+ sortBy := c.Query("sort_by")
+ sortOrder := c.Query("sort_order")
+
+ listData := service_event.AttendanceListData{
+ EventId: eventId,
+ Name: c.Query("name"),
+ KycStatus: c.Query("kyc_status"),
+ Limit: &limit,
+ Offset: &offset,
+ SortBy: &sortBy,
+ SortOrder: &sortOrder,
+ }
+
+ result := self.svc.AttendanceList(&service_event.AttendanceListPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &listData,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/event/checkin.go b/api/event/checkin.go
new file mode 100644
index 0000000..b469979
--- /dev/null
+++ b/api/event/checkin.go
@@ -0,0 +1,190 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "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
+// @Security Bearer
+// @Param event_id query string true "Event UUID"
+// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
+// @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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "checkin",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventCheckin)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ eventIdOrig := c.Query("event_id")
+ eventId, err := uuid.Parse(eventIdOrig)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Checkin(&service_event.CheckinPayload{
+ Context: ctx,
+ 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
+// @Security Bearer
+// @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"
+// @Router /event/checkin/submit [post]
+func (self *EventHandler) CheckinSubmit(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "checkin_submit",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventCheckinSubmit)
+
+ var data service_event.CheckinSubmitData
+ if err := c.ShouldBindJSON(&data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.CheckinSubmit(&service_event.CheckinSubmitPayload{
+ Context: ctx,
+ 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} 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]
+func (self *EventHandler) CheckinQuery(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "checkin_query",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventCheckinQuery)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ eventIdOrig := c.Query("event_id")
+ eventId, err := uuid.Parse(eventIdOrig)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.CheckinQuery(&service_event.CheckinQueryPayload{
+ Context: ctx,
+ 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/create.go b/api/event/create.go
new file mode 100644
index 0000000..99a8f8d
--- /dev/null
+++ b/api/event/create.go
@@ -0,0 +1,91 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Create handles the request to create a new event.
+//
+// @Summary Create an Event
+// @Description Allows a Lv30+ user to create a new event. Users at exactly Lv30 may only create events with type 'party'. Sets type and enable_kyc, which are immutable after creation.
+// @Tags Event
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param request body service_event.EventCreateData true "Event Creation Details"
+// @Success 200 {object} utils.RespStatus{data=service_event.EventCreateResponse} "Successfully created the event"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Permission Denied / Type Not Allowed for this level"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / Database Error"
+// @Router /event/create [post]
+func (self *EventHandler) Create(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "create",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventCreate)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ permissionLevelOrig, ok := c.Get("permission_level")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Missing PermissionLevel")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ var createData service_event.EventCreateData
+ if err := c.ShouldBindJSON(&createData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ createData.UserId = userIdOrig.(string)
+ createData.PermissionLevel = permissionLevelOrig.(uint)
+
+ payload := &service_event.EventCreatePayload{
+ Context: ctx,
+ Data: &createData,
+ }
+
+ result := self.svc.Create(payload)
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/event/delete.go b/api/event/delete.go
new file mode 100644
index 0000000..e95f746
--- /dev/null
+++ b/api/event/delete.go
@@ -0,0 +1,74 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Delete removes an event by event_id.
+//
+// @Summary Delete an Event
+// @Description Permanently deletes an event. Requires Lv40+.
+// @Tags Event
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param request body service_event.EventDeleteData true "Event to delete"
+// @Success 200 {object} utils.RespStatus{data=nil} "Successfully deleted"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /event/delete [delete]
+func (self *EventHandler) Delete(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "delete",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventDelete)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ _, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ var deleteData service_event.EventDeleteData
+ if err := c.ShouldBindJSON(&deleteData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.Delete(&service_event.EventDeletePayload{
+ Context: ctx,
+ Data: &deleteData,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/event/event_handler_test.go b/api/event/event_handler_test.go
new file mode 100644
index 0000000..65b602f
--- /dev/null
+++ b/api/event/event_handler_test.go
@@ -0,0 +1,608 @@
+package event
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+func issueToken(t *testing.T, userId uuid.UUID) string {
+ t.Helper()
+ tok := &authtoken.Token{Application: viper.GetString("server.application")}
+ access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId)
+ require.NoError(t, err)
+ return access
+}
+
+func newEventRouter(t *testing.T) *gin.Engine {
+ t.Helper()
+ r := gin.New()
+ ApiHandler(r.Group("/event"))
+ return r
+}
+
+func postJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func getRequest(t *testing.T, r *gin.Engine, path string) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func patchJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func deleteReq(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodDelete, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func postWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func getWithBearer(t *testing.T, r *gin.Engine, path, token string) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func patchWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func deleteWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodDelete, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+// ---- Create ----
+
+func TestEventCreateHandler(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{
+ Type: "official",
+ Name: "Handler Test Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ data, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.NotEmpty(t, data["event_id"])
+}
+
+func TestEventCreateHandlerMissingAuth(t *testing.T) {
+ testutil.Setup(t)
+ r := newEventRouter(t)
+
+ w := postJSON(t, r, "/event/create", map[string]any{"type": "party"})
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
+
+func TestEventCreateHandlerInvalidJSON(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ token := issueToken(t, owner.UserId)
+ r := newEventRouter(t)
+
+ req := httptest.NewRequest(http.MethodPost, "/event/create", bytes.NewBufferString("{bad json"))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Info ----
+
+func TestEventInfoHandlerNotFound(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/info?event_id=00000000-0000-0000-0000-000000000001", token)
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
+
+// ---- List ----
+
+func TestEventListHandlerRequiresOffset(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/list", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestEventListHandlerWithOffset(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/list?offset=0&limit=10", token)
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- Join ----
+
+func TestEventJoinHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(t.Context(), user.UserId, data.WithNickname("Joiner")))
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := postWithBearer(t, r, "/event/join", token, map[string]any{"event_id": "not-a-uuid"})
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Delete ----
+
+func TestEventDeleteHandlerNotFound(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ nonExistentId := uuid.New().String()
+ w := deleteWithBearer(t, r, "/event/delete", token, map[string]any{
+ "event_id": nonExistentId,
+ })
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
+
+func TestEventDeleteHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ token := issueToken(t, owner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{
+ Type: "party",
+ Name: "Delete Me",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ w := deleteWithBearer(t, r, "/event/delete", token, map[string]any{"event_id": eventId})
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- Info (success) ----
+
+func TestEventInfoHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ ownerToken := issueToken(t, owner.UserId)
+ userToken := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{
+ Type: "party",
+ Name: "Info Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ w := getWithBearer(t, r, "/event/info?event_id="+eventId, userToken)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ d, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.Equal(t, eventId, d["event_id"])
+}
+
+// ---- Join (success) ----
+
+func TestEventJoinHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ joiner := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(t.Context(), joiner.UserId, data.WithNickname("Joiner")))
+
+ ownerToken := issueToken(t, owner.UserId)
+ joinerToken := issueToken(t, joiner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{
+ Type: "party",
+ Name: "Join Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ w := postWithBearer(t, r, "/event/join", joinerToken, map[string]any{"event_id": eventId})
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- Update ----
+
+func TestEventUpdateHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, owner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{
+ Type: "party",
+ Name: "Original Name",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ newName := "Updated Name"
+ w := patchWithBearer(t, r, "/event/update", token, map[string]any{
+ "event_id": eventId,
+ "name": newName,
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+func TestEventUpdateHandlerInvalidJSON(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ req := httptest.NewRequest(http.MethodPatch, "/event/update", bytes.NewBufferString("{bad"))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Stats ----
+
+func TestEventStatsHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, owner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{
+ Type: "party",
+ Name: "Stats Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ w := getWithBearer(t, r, "/event/stats?event_id="+eventId, token)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ d, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.NotNil(t, d["join_count"])
+}
+
+func TestEventStatsHandlerMissingEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/stats?event_id=not-a-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Checkin ----
+
+func TestEventCheckinHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/checkin?event_id=not-a-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestEventCheckinHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ joiner := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(t.Context(), joiner.UserId, data.WithNickname("Joiner")))
+
+ ownerToken := issueToken(t, owner.UserId)
+ joinerToken := issueToken(t, joiner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{
+ Type: "party",
+ Name: "Checkin Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ joinW := postWithBearer(t, r, "/event/join", joinerToken, map[string]any{"event_id": eventId})
+ require.Equal(t, http.StatusOK, joinW.Code)
+
+ w := getWithBearer(t, r, "/event/checkin?event_id="+eventId, joinerToken)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ d, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.NotEmpty(t, d["checkin_code"])
+}
+
+// ---- CheckinSubmit ----
+
+func TestEventCheckinSubmitHandlerInvalidCode(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 20)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := postWithBearer(t, r, "/event/checkin/submit", token, service_event.CheckinSubmitData{
+ CheckinCode: "000000",
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestEventCheckinSubmitHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ joiner := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(t.Context(), joiner.UserId, data.WithNickname("Joiner")))
+
+ ownerToken := issueToken(t, owner.UserId)
+ joinerToken := issueToken(t, joiner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", ownerToken, service_event.EventCreateData{
+ Type: "party",
+ Name: "Checkin Submit Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ joinW := postWithBearer(t, r, "/event/join", joinerToken, map[string]any{"event_id": eventId})
+ require.Equal(t, http.StatusOK, joinW.Code)
+
+ checkinW := getWithBearer(t, r, "/event/checkin?event_id="+eventId, joinerToken)
+ require.Equal(t, http.StatusOK, checkinW.Code)
+ var checkinResp map[string]any
+ require.NoError(t, json.Unmarshal(checkinW.Body.Bytes(), &checkinResp))
+ code := checkinResp["data"].(map[string]any)["checkin_code"].(string)
+
+ w := postWithBearer(t, r, "/event/checkin/submit", ownerToken, service_event.CheckinSubmitData{
+ CheckinCode: code,
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- CheckinQuery ----
+
+func TestEventCheckinQueryHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/checkin/query?event_id=not-a-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestEventCheckinQueryHandlerNotJoined(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/checkin/query?event_id="+uuid.New().String(), token)
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
+
+// ---- Attendance ----
+
+func TestEventAttendanceHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := getWithBearer(t, r, "/event/attendance?event_id=not-a-uuid", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestEventAttendanceHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, owner.UserId)
+ r := newEventRouter(t)
+
+ createW := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{
+ Type: "party",
+ Name: "Attendance Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ })
+ require.Equal(t, http.StatusOK, createW.Code)
+ var createResp map[string]any
+ require.NoError(t, json.Unmarshal(createW.Body.Bytes(), &createResp))
+ eventId := createResp["data"].(map[string]any)["event_id"].(string)
+
+ w := getWithBearer(t, r, "/event/attendance?event_id="+eventId, token)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ d, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.NotNil(t, d["total"])
+}
+
+// ---- Guide ----
+
+func TestEventGuideHandlerInvalidEventId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ // The guide handler returns 500 for invalid UUID (source behaviour)
+ w := getWithBearer(t, r, "/event/guide?event_id=not-a-uuid", token)
+ assert.NotEqual(t, http.StatusNotFound, w.Code, "route must be registered")
+}
+
+// ---- Permission enforcement ----
+
+func TestEventCreateHandlerLowPermission(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := postWithBearer(t, r, "/event/create", token, service_event.EventCreateData{
+ Type: "party",
+ Name: "Should Fail",
+ Quota: 10,
+ Limit: 20,
+ })
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
+
+func TestEventDeleteHandlerLowPermission(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ token := issueToken(t, user.UserId)
+ r := newEventRouter(t)
+
+ w := deleteWithBearer(t, r, "/event/delete", token, map[string]any{
+ "event_id": uuid.New().String(),
+ })
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
diff --git a/api/event/guide.go b/api/event/guide.go
new file mode 100644
index 0000000..cd60085
--- /dev/null
+++ b/api/event/guide.go
@@ -0,0 +1,92 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Specific guide about a specific event.
+//
+// @Summary Get Event Guide
+// @Description Fetching attendance guide of an event using its UUID.
+// @Tags Event
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param event_id query string true "Event UUID"
+// @Success 200 {object} utils.RespStatus{data=service_event.AttendanceGuideResponse} "Successful retrieval"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /event/guide [get]
+func (self *EventHandler) Guide(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "guide",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventAttendanceGuide)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ eventIdOrig := c.Query("event_id")
+ eventId, err := uuid.Parse(eventIdOrig)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ result := self.svc.GetAttendanceGuide(&service_event.AttendanceGuidePayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &service_event.AttendanceGuideData{
+ 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/event/handler.go b/api/event/handler.go
index 8e182c5..525ab32 100644
--- a/api/event/handler.go
+++ b/api/event/handler.go
@@ -2,10 +2,44 @@ 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}
+
+ // Lv10+ routes
+ lv10 := r.Group("")
+ lv10.Use(middleware.JWTAuth(), middleware.Permission(10))
+ lv10.GET("/info", eventHandler.Info)
+ lv10.GET("/checkin", eventHandler.Checkin)
+ lv10.GET("/checkin/query", eventHandler.CheckinQuery)
+ lv10.POST("/join", eventHandler.Join)
+ lv10.GET("/guide", eventHandler.Guide)
+ lv10.GET("/list", eventHandler.List)
+
+ // Lv20+ routes
+ lv20 := r.Group("")
+ lv20.Use(middleware.JWTAuth(), middleware.Permission(20))
+ lv20.POST("/checkin/submit", eventHandler.CheckinSubmit)
+
+ // Lv30+ routes
+ lv30 := r.Group("")
+ lv30.Use(middleware.JWTAuth(), middleware.Permission(30))
+ lv30.POST("/create", eventHandler.Create)
+ lv30.PATCH("/update", eventHandler.Update)
+ lv30.GET("/attendance", eventHandler.Attendance)
+ lv30.GET("/stats", eventHandler.Stats)
+
+ // Lv40+ routes
+ lv40 := r.Group("")
+ lv40.Use(middleware.JWTAuth(), middleware.Permission(40))
+ lv40.DELETE("/delete", eventHandler.Delete)
}
diff --git a/api/event/info.go b/api/event/info.go
new file mode 100644
index 0000000..f6334ff
--- /dev/null
+++ b/api/event/info.go
@@ -0,0 +1,103 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "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
+// @Security Bearer
+// @Param event_id query string true "Event UUID"
+// @Success 200 {object} utils.RespStatus{data=service_event.EventInfoResponse} "Successful retrieval"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
+// @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) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "info",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventInfo)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ eventIdOrig := c.Query("event_id")
+ if eventIdOrig == "" {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingEventId),
+ exception.WithError(errors.New("Missing EventId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ eventId, err := uuid.Parse(eventIdOrig)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ result := self.svc.GetInfo(&service_event.EventInfoPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &service_event.EventInfoData{
+ 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/event/join.go b/api/event/join.go
new file mode 100644
index 0000000..0fb5723
--- /dev/null
+++ b/api/event/join.go
@@ -0,0 +1,78 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Join handles the request for a user to join a specific event.
+//
+// @Summary Join an Event
+// @Description Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.
+// @Tags Event
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param request body service_event.EventJoinData true "Event Join Details (UserId and EventId are required)"
+// @Success 200 {object} utils.RespStatus{data=service_event.EventJoinResponse} "Successfully joined the event"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input or UUID Parse Failed"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized / Missing User ID / Event Limit Exceeded"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / Database Error"
+// @Router /event/join [post]
+func (self *EventHandler) Join(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "join",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventJoin)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ var joinData service_event.EventJoinData
+ if err := c.ShouldBindJSON(&joinData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ joinData.UserId = userIdOrig.(string)
+
+ payload := &service_event.EventJoinPayload{
+ Context: ctx,
+ Data: &joinData,
+ }
+
+ result := self.svc.Join(payload)
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/event/list.go b/api/event/list.go
new file mode 100644
index 0000000..9c78f8a
--- /dev/null
+++ b/api/event/list.go
@@ -0,0 +1,118 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// List retrieves a paginated, filterable list of events.
+//
+// @Summary List Events
+// @Description Returns a paginated list of events. Supports filtering by type and sorting. Lv30 users are automatically scoped to events they own.
+// @Tags Event
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param limit query int false "Maximum number of events to return (default 20)"
+// @Param offset query int true "Number of events to skip"
+// @Param type query string false "Filter by event type: 'official' or 'party'"
+// @Param sort_by query string false "Sort field: 'start_time' (default), 'end_time', 'name'"
+// @Param sort_order query string false "Sort direction: 'asc' or 'desc' (default)"
+// @Success 200 {object} utils.RespStatus{data=service_event.EventListResponse} "Successful paginated list retrieval"
+// @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 /event/list [get]
+func (self *EventHandler) List(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventList)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ permissionLevelOrig, ok := c.Get("permission_level")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Missing PermissionLevel")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ type ListQuery struct {
+ Limit *string `form:"limit"`
+ Offset *string `form:"offset"`
+ Type *string `form:"type"`
+ SortBy *string `form:"sort_by"`
+ SortOrder *string `form:"sort_order"`
+ }
+
+ var query ListQuery
+ if err := c.ShouldBindQuery(&query); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusClient),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.List(&service_event.EventListPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &service_event.EventListData{
+ Limit: query.Limit,
+ Offset: query.Offset,
+ Type: query.Type,
+ SortBy: query.SortBy,
+ SortOrder: query.SortOrder,
+ PermissionLevel: permissionLevelOrig.(uint),
+ },
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/event/stats.go b/api/event/stats.go
new file mode 100644
index 0000000..71e4930
--- /dev/null
+++ b/api/event/stats.go
@@ -0,0 +1,78 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Stats returns aggregate statistics for an event.
+//
+// @Summary Get Event Statistics
+// @Description Returns join count, checkin count, KYC pass rate, and agenda submission count. Only accessible by the event owner (Manager).
+// @Tags Event
+// @Produce json
+// @Security Bearer
+// @Param event_id query string true "Event UUID"
+// @Success 200 {object} utils.RespStatus{data=service_event.EventStatsResponse} "Statistics retrieved successfully"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Not Event Owner"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /event/stats [get]
+func (self *EventHandler) Stats(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "stats",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventStats)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userId, err := uuid.Parse(userIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ result := self.svc.Stats(&service_event.EventStatsPayload{
+ Context: ctx,
+ UserId: userId,
+ Data: &service_event.EventStatsData{
+ EventId: c.Query("event_id"),
+ },
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/event/update.go b/api/event/update.go
new file mode 100644
index 0000000..83ebfb8
--- /dev/null
+++ b/api/event/update.go
@@ -0,0 +1,90 @@
+package event
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_event"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Update modifies editable fields of an event owned by the requesting user.
+//
+// @Summary Update an Event
+// @Description Allows the event owner (Manager) to update name, subtitle, description, start_time, end_time, thumbnail, and is_agenda_published. Lv40+ users (admins) bypass the owner restriction and may update any event. Changes to type or enable_kyc are rejected. is_agenda_published is write-once: it can only be set to true (requires at least one agenda submission) and cannot be reverted.
+// @Tags Event
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param request body service_event.EventUpdateData true "Fields to update (all optional except event_id)"
+// @Success 200 {object} utils.RespStatus{data=nil} "Successfully updated"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Immutable Field / Agenda Pre-flight Failed"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Not Event Owner"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /event/update [patch]
+func (self *EventHandler) Update(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_event",
+ "update",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointEventUpdate)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ permissionLevelOrig, ok := c.Get("permission_level")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Missing PermissionLevel")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ var updateData service_event.EventUpdateData
+ if err := c.ShouldBindJSON(&updateData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ updateData.UserId = userIdOrig.(string)
+ updateData.PermissionLevel = permissionLevelOrig.(uint)
+
+ result := self.svc.Update(&service_event.EventUpdatePayload{
+ Context: ctx,
+ Data: &updateData,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/handler.go b/api/handler.go
index 883fca7..bead84a 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -1,9 +1,11 @@
package api
import (
+ "nixcn-cms/api/agenda"
"nixcn-cms/api/auth"
"nixcn-cms/api/event"
"nixcn-cms/api/kyc"
+ "nixcn-cms/api/stats"
"nixcn-cms/api/user"
"github.com/gin-gonic/gin"
@@ -14,4 +16,6 @@ func Handler(r *gin.RouterGroup) {
user.ApiHandler(r.Group("/user"))
event.ApiHandler(r.Group("/event"))
kyc.ApiHandler(r.Group("/kyc"))
+ agenda.ApiHandler(r.Group("/agenda"))
+ stats.ApiHandler(r.Group("/stats"))
}
diff --git a/api/kyc/handler.go b/api/kyc/handler.go
index 452c43d..9358235 100644
--- a/api/kyc/handler.go
+++ b/api/kyc/handler.go
@@ -2,10 +2,20 @@ package kyc
import (
"nixcn-cms/middleware"
+ "nixcn-cms/service/service_kyc"
"github.com/gin-gonic/gin"
)
-func ApiHandler(r *gin.RouterGroup) {
- r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
+type KycHandler struct {
+ svc service_kyc.KycService
+}
+
+func ApiHandler(r *gin.RouterGroup) {
+ kycSvc := service_kyc.NewKycService()
+ kycHandler := &KycHandler{kycSvc}
+
+ r.Use(middleware.JWTAuth(), middleware.Permission(10))
+ r.POST("/session", kycHandler.Session)
+ r.POST("/query", kycHandler.Query)
}
diff --git a/api/kyc/kyc_handler_test.go b/api/kyc/kyc_handler_test.go
new file mode 100644
index 0000000..a93f391
--- /dev/null
+++ b/api/kyc/kyc_handler_test.go
@@ -0,0 +1,132 @@
+package kyc
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/service/service_kyc"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+func issueToken(t *testing.T, userId uuid.UUID) string {
+ t.Helper()
+ tok := &authtoken.Token{Application: viper.GetString("server.application")}
+ access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId)
+ require.NoError(t, err)
+ return access
+}
+
+func newKycRouter(t *testing.T) *gin.Engine {
+ t.Helper()
+ r := gin.New()
+ ApiHandler(r.Group("/kyc"))
+ return r
+}
+
+func postWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+// ---- Session ----
+
+func TestKycSessionHandlerNoAuth(t *testing.T) {
+ testutil.Setup(t)
+ r := newKycRouter(t)
+
+ w := postWithBearer(t, r, "/kyc/session", "", map[string]any{
+ "type": "cnrid",
+ "identity": "dGVzdA==",
+ })
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
+
+func TestKycSessionHandlerInvalidJSON(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newKycRouter(t)
+
+ req := httptest.NewRequest(http.MethodPost, "/kyc/session", bytes.NewBufferString("{bad"))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestKycSessionHandlerInvalidBase64Identity(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newKycRouter(t)
+
+ w := postWithBearer(t, r, "/kyc/session", token, service_kyc.KycSessionData{
+ Type: "cnrid",
+ Identity: "!!!not-base64!!!",
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestKycSessionHandlerInvalidType(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newKycRouter(t)
+
+ validB64 := base64.StdEncoding.EncodeToString([]byte(`{}`))
+ w := postWithBearer(t, r, "/kyc/session", token, service_kyc.KycSessionData{
+ Type: "unknown_type",
+ Identity: validB64,
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+// ---- Query ----
+
+func TestKycQueryHandlerNoAuth(t *testing.T) {
+ testutil.Setup(t)
+ r := newKycRouter(t)
+
+ w := postWithBearer(t, r, "/kyc/query", "", map[string]any{
+ "kyc_id": "00000000-0000-0000-0000-000000000001",
+ })
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
+
+func TestKycQueryHandlerInvalidJSON(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newKycRouter(t)
+
+ req := httptest.NewRequest(http.MethodPost, "/kyc/query", bytes.NewBufferString("{bad"))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
diff --git a/api/kyc/query.go b/api/kyc/query.go
new file mode 100644
index 0000000..136a9e9
--- /dev/null
+++ b/api/kyc/query.go
@@ -0,0 +1,73 @@
+package kyc
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_kyc"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Summary Query KYC Status
+// @Description Checks the current state of a KYC session and updates local database if approved.
+// @Tags KYC
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param payload body service_kyc.KycQueryData true "KYC query data (KycId)"
+// @Success 200 {object} utils.RespStatus{data=service_kyc.KycQueryResponse} "Query processed (success/pending/failed)"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid UUID or input"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /kyc/query [post]
+func (self *KycHandler) Query(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_kyc",
+ "query",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointKycQuery)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ _, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ var queryData service_kyc.KycQueryData
+ if err := c.ShouldBindJSON(&queryData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ queryPayload := &service_kyc.KycQueryPayload{
+ Context: ctx,
+ Data: &queryData,
+ }
+
+ result := self.svc.QueryKyc(queryPayload)
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/kyc/session.go b/api/kyc/session.go
new file mode 100644
index 0000000..b6f3a48
--- /dev/null
+++ b/api/kyc/session.go
@@ -0,0 +1,75 @@
+package kyc
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_kyc"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// @Summary Create KYC Session
+// @Description Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.
+// @Tags KYC
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param payload body service_kyc.KycSessionData true "KYC session data (Type and Base64 Identity)"
+// @Success 200 {object} utils.RespStatus{data=service_kyc.KycSessionResponse} "Session created successfully"
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid input or decode failed"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / KYC Service Error"
+// @Router /kyc/session [post]
+func (self *KycHandler) Session(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_kyc",
+ "session",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointKycSession)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdFromHeaderOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ var sessionData service_kyc.KycSessionData
+ if err := c.ShouldBindJSON(&sessionData); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ sessionData.UserId = userIdFromHeaderOrig.(string)
+
+ kycPayload := &service_kyc.KycSessionPayload{
+ Context: ctx,
+ Data: &sessionData,
+ }
+
+ result := self.svc.SessionKyc(kycPayload)
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/stats/global.go b/api/stats/global.go
new file mode 100644
index 0000000..2167f6f
--- /dev/null
+++ b/api/stats/global.go
@@ -0,0 +1,44 @@
+package stats
+
+import (
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_stats"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// Global returns platform-wide statistics. Lv40+ only.
+//
+// @Summary Global Stats
+// @Description Returns total users, user counts per permission_level, and per-event join/checkin counts.
+// @Tags Stats
+// @Produce json
+// @Security Bearer
+// @Success 200 {object} utils.RespStatus{data=service_stats.GlobalStatsResponse}
+// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /stats/global [get]
+func (self *StatsHandler) Global(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_stats",
+ "global",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointStatsGlobal)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ result := self.svc.Global(&service_stats.GlobalStatsPayload{
+ Context: ctx,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/stats/handler.go b/api/stats/handler.go
new file mode 100644
index 0000000..ce2e70c
--- /dev/null
+++ b/api/stats/handler.go
@@ -0,0 +1,22 @@
+package stats
+
+import (
+ "nixcn-cms/middleware"
+ "nixcn-cms/service/service_stats"
+
+ "github.com/gin-gonic/gin"
+)
+
+type StatsHandler struct {
+ svc service_stats.StatsService
+}
+
+func ApiHandler(r *gin.RouterGroup) {
+ statsSvc := service_stats.NewStatsService()
+ statsHandler := &StatsHandler{statsSvc}
+
+ // Lv40+ routes
+ lv40 := r.Group("")
+ lv40.Use(middleware.JWTAuth(), middleware.Permission(40))
+ lv40.GET("/global", statsHandler.Global)
+}
diff --git a/api/stats/stats_handler_test.go b/api/stats/stats_handler_test.go
new file mode 100644
index 0000000..e330679
--- /dev/null
+++ b/api/stats/stats_handler_test.go
@@ -0,0 +1,66 @@
+package stats
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+func issueToken(t *testing.T, userId uuid.UUID) string {
+ t.Helper()
+ tok := &authtoken.Token{Application: viper.GetString("server.application")}
+ access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId)
+ require.NoError(t, err)
+ return access
+}
+
+func newStatsRouter(t *testing.T) *gin.Engine {
+ t.Helper()
+ r := gin.New()
+ ApiHandler(r.Group("/stats"))
+ return r
+}
+
+func TestGlobalStatsHandler(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ admin := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ token := issueToken(t, admin.UserId)
+ r := newStatsRouter(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/stats/global", nil)
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ data, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ _, hasTotalUsers := data["total_users"]
+ assert.True(t, hasTotalUsers)
+}
+
+func TestGlobalStatsHandlerNoAuth(t *testing.T) {
+ testutil.Setup(t)
+ r := newStatsRouter(t)
+
+ req := httptest.NewRequest(http.MethodGet, "/stats/global", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+}
diff --git a/api/user/admin_update.go b/api/user/admin_update.go
new file mode 100644
index 0000000..cf3aa9a
--- /dev/null
+++ b/api/user/admin_update.go
@@ -0,0 +1,115 @@
+package user
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_user"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// AdminUpdate modifies another user's profile. Lv40+ only.
+//
+// @Summary Admin Update User
+// @Description Lv40+ operators may update any user with a strictly lower permission_level. Editable fields: all profile fields plus permission_level (new value must be below operator's own level).
+// @Tags User
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param user_id path string true "Target User ID"
+// @Param payload body service_user.UserInfoUpdateData true "Fields to update"
+// @Success 200 {object} utils.RespStatus{data=nil}
+// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
+// @Failure 403 {object} utils.RespStatus{data=nil} "Permission Matrix Violation"
+// @Failure 404 {object} utils.RespStatus{data=nil} "Target User Not Found"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
+// @Router /user/update/{user_id} [patch]
+func (self *UserHandler) AdminUpdate(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_user",
+ "admin_update",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointUserAdminUpdate)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ operatorIdOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ operatorId, err := uuid.Parse(operatorIdOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ permissionLevelOrig, ok := c.Get("permission_level")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Missing PermissionLevel")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ targetId, err := uuid.Parse(c.Param("user_id"))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("invalid user_id")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ var data service_user.UserInfoData
+ if err := c.ShouldBindJSON(&data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+
+ result := self.svc.UpdateInfo(&service_user.UserInfoPayload{
+ Context: ctx,
+ UserId: targetId,
+ OperatorId: operatorId,
+ OperatorLevel: permissionLevelOrig.(uint),
+ Data: &data,
+ })
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+}
diff --git a/api/user/create.go b/api/user/create.go
deleted file mode 100644
index dafe823..0000000
--- a/api/user/create.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package user
-
-import "github.com/gin-gonic/gin"
-
-func (self *UserHandler) Create(c *gin.Context) {
-}
diff --git a/api/user/full.go b/api/user/full.go
deleted file mode 100644
index 5c434af..0000000
--- a/api/user/full.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package user
-
-import (
- "nixcn-cms/internal/exception"
- "nixcn-cms/service"
- "nixcn-cms/utils"
-
- "github.com/gin-gonic/gin"
-)
-
-func (self *UserHandler) Full(c *gin.Context) {
- userTablePayload := &service.UserTablePayload{
- Context: c,
- }
-
- result := self.svc.GetUserFullTable(userTablePayload)
-
- if result.Common.Exception.Original != exception.CommonSuccess {
- utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
- return
- }
-
- utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
-}
diff --git a/api/user/handler.go b/api/user/handler.go
index 1d2ac0f..ccfdac9 100644
--- a/api/user/handler.go
+++ b/api/user/handler.go
@@ -2,23 +2,29 @@ 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))
- r.GET("/info", userHandler.Info)
- r.PATCH("/update", userHandler.Update)
- r.GET("/list", middleware.Permission(20), userHandler.List)
- r.POST("/full", middleware.Permission(40), userHandler.Full)
- r.POST("/create", middleware.Permission(50), userHandler.Create)
+ // Lv5+ routes
+ lv5 := r.Group("")
+ lv5.Use(middleware.JWTAuth(), middleware.Permission(5))
+ lv5.GET("/info", userHandler.Info)
+ lv5.GET("/info/:user_id", userHandler.Other)
+ lv5.PATCH("/update", userHandler.Update)
+
+ // Lv40+ routes
+ lv40 := r.Group("")
+ lv40.Use(middleware.JWTAuth(), middleware.Permission(40))
+ lv40.GET("/list", userHandler.List)
+ lv40.PATCH("/update/:user_id", userHandler.AdminUpdate)
}
diff --git a/api/user/info.go b/api/user/info.go
index 2e506e8..461f5f5 100644
--- a/api/user/info.go
+++ b/api/user/info.go
@@ -1,51 +1,72 @@
package user
import (
+ "errors"
"nixcn-cms/internal/exception"
- "nixcn-cms/service"
+ "nixcn-cms/service/service_user"
+ "nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"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
+// @Security Bearer
+// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
+// @Failure 401 {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)"
+// @Router /user/info [get]
func (self *UserHandler) Info(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_user",
+ "info",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointUserInfo)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
userIdOrig, ok := c.Get("user_id")
if !ok {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.ServiceUser).
- SetEndpoint(exception.EndpointUserServiceInfo).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorMissingUserId).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.ServiceUser).
- SetEndpoint(exception.EndpointUserServiceInfo).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorUuidParseFailed).
- SetError(err).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
- UserInfoPayload := &service.UserInfoPayload{
- Context: c,
+ UserInfoPayload := &service_user.UserInfoPayload{
+ Context: ctx,
UserId: userId,
+ IsOther: false,
Data: nil,
}
- result := self.svc.GetUserInfo(UserInfoPayload)
+ result := self.svc.GetInfo(UserInfoPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
diff --git a/api/user/list.go b/api/user/list.go
index 0c4eeee..4388e9f 100644
--- a/api/user/list.go
+++ b/api/user/list.go
@@ -2,45 +2,98 @@ package user
import (
"nixcn-cms/internal/exception"
- "nixcn-cms/service"
+ "nixcn-cms/service/service_user"
+ "nixcn-cms/tracer"
"nixcn-cms/utils"
+ "strconv"
"github.com/gin-gonic/gin"
)
+// List retrieves a paginated, filterable list of users. Lv40+ only.
+//
+// @Summary List Users (Admin)
+// @Description Returns a paginated list of users with permission_level included. Supports filtering by permission_level and sorting.
+// @Tags User
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param limit query string false "Maximum number of users to return (default 20)"
+// @Param offset query string true "Number of users to skip"
+// @Param sort_by query string false "Sort field: 'id' (default) | 'permission_level'"
+// @Param sort_order query string false "Sort direction: 'asc' (default) | 'desc'"
+// @Param permission_level query int false "Filter by exact permission level"
+// @Success 200 {object} utils.RespStatus{data=[]service_user.UserListResponse} "Successful paginated list retrieval"
+// @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 /user/list [get]
func (self *UserHandler) List(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_user",
+ "list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointUserList)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
type ListQuery struct {
- Limit *string `form:"limit"`
- Offset *string `form:"offset"`
+ Limit *string `form:"limit"`
+ Offset *string `form:"offset"`
+ SortBy *string `form:"sort_by"`
+ SortOrder *string `form:"sort_order"`
+ PermissionLevel *string `form:"permission_level"`
}
var query ListQuery
if err := c.ShouldBindQuery(&query); err != nil {
- exception := new(exception.Builder).
- SetStatus(exception.StatusClient).
- SetService(exception.ServiceUser).
- SetEndpoint(exception.EndpointUserServiceList).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorInvalidInput).
- Throw(c).
- String()
-
- utils.HttpResponse(c, 400, exception)
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusClient),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
return
}
- userListPayload := &service.UserListPayload{
- Context: c,
- Limit: query.Limit,
- Offset: query.Offset,
+ var permLevel *uint
+ if query.PermissionLevel != nil && *query.PermissionLevel != "" {
+ v, err := strconv.ParseUint(*query.PermissionLevel, 10, 64)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 400, errorCode)
+ return
+ }
+ u := uint(v)
+ permLevel = &u
}
- result := self.svc.ListUsers(userListPayload)
+ result := self.svc.List(&service_user.UserListPayload{
+ Context: ctx,
+ Data: &service_user.UserListData{
+ Limit: query.Limit,
+ Offset: query.Offset,
+ SortBy: query.SortBy,
+ SortOrder: query.SortOrder,
+ PermissionLevel: permLevel,
+ },
+ })
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
- utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), gin.H{
+ "total": result.Total,
+ "items": result.Data,
+ })
}
diff --git a/api/user/other.go b/api/user/other.go
new file mode 100644
index 0000000..adfef06
--- /dev/null
+++ b/api/user/other.go
@@ -0,0 +1,103 @@
+package user
+
+import (
+ "errors"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/service_user"
+ "nixcn-cms/tracer"
+ "nixcn-cms/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+)
+
+// Info retrieves the profile information of the other user.
+//
+// @Summary Get Other User Information
+// @Description Fetches the complete profile data for the user associated with the provided session/token.
+// @Tags User
+// @Accept json
+// @Produce json
+// @Security Bearer
+// @Param user_id path string true "Other user id"
+// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
+// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
+// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found"
+// @Failure 403 {object} utils.RespStatus{data=nil} "User Not Public"
+// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)"
+// @Router /user/info/{user_id} [get]
+func (self *UserHandler) Other(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_user",
+ "other",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointUserInfo)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
+ userIdFromUrlOrig := c.Param("user_id")
+
+ userIdFromUrl, err := uuid.Parse(userIdFromUrlOrig)
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ userIdFromHeaderOrig, ok := c.Get("user_id")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
+ }
+
+ userIdFromHeader, err := uuid.Parse(userIdFromHeaderOrig.(string))
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 500, errorCode)
+ return
+ }
+
+ var UserInfoPayload = &service_user.UserInfoPayload{}
+ if userIdFromUrl == userIdFromHeader {
+ UserInfoPayload = &service_user.UserInfoPayload{
+ Context: ctx,
+ UserId: userIdFromHeader,
+ IsOther: false,
+ Data: nil,
+ }
+ } else if userIdFromUrl != userIdFromHeader {
+ UserInfoPayload = &service_user.UserInfoPayload{
+ Context: ctx,
+ UserId: userIdFromUrl,
+ IsOther: true,
+ Data: nil,
+ }
+ }
+
+ result := self.svc.GetInfo(UserInfoPayload)
+
+ if result.Common.Exception.Original != exception.CommonSuccess {
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
+ return
+ }
+
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+}
diff --git a/api/user/update.go b/api/user/update.go
index 18ab94a..4927751 100644
--- a/api/user/update.go
+++ b/api/user/update.go
@@ -1,69 +1,102 @@
package user
import (
+ "errors"
"nixcn-cms/internal/exception"
- "nixcn-cms/service"
+ "nixcn-cms/service/service_user"
+ "nixcn-cms/tracer"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"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
+// @Security Bearer
+// @Param payload body service_user.UserInfoUpdateData true "Updated User Profile Data"
+// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update"
+// @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 /user/update [patch]
func (self *UserHandler) Update(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "api_user",
+ "update",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointUserUpdate)
+ ctx = exception.ContextWithService(ctx, exception.ServiceEndpoint)
+
userIdOrig, ok := c.Get("user_id")
if !ok {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.ServiceUser).
- SetEndpoint(exception.EndpointUserServiceUpdate).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorMissingUserId).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.ServiceUser).
- SetEndpoint(exception.EndpointUserServiceUpdate).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorUuidParseFailed).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
utils.HttpResponse(c, 500, errorCode)
return
}
- userInfoPayload := &service.UserInfoPayload{
- Context: c,
- UserId: userId,
+ permissionLevelOrig, ok := c.Get("permission_level")
+ if !ok {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Missing PermissionLevel")),
+ ).Throw(ctx).String()
+ utils.HttpResponse(c, 403, errorCode)
+ return
}
- err = c.ShouldBindJSON(&userInfoPayload.Data)
- if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.ServiceUser).
- SetEndpoint(exception.EndpointUserServiceUpdate).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorInvalidInput).
- SetError(err).
- Throw(c).
- String()
+ payload := &service_user.UserInfoPayload{
+ Context: ctx,
+ UserId: userId,
+ OperatorId: userId,
+ OperatorLevel: permissionLevelOrig.(uint),
+ }
+
+ if err := c.ShouldBindJSON(&payload.Data); err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx).String()
utils.HttpResponse(c, 400, errorCode)
return
}
- result := self.svc.UpdateUserInfo(userInfoPayload)
+ result := self.svc.UpdateInfo(payload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
- utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
+ utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
}
diff --git a/api/user/user_handler_test.go b/api/user/user_handler_test.go
new file mode 100644
index 0000000..9f6a740
--- /dev/null
+++ b/api/user/user_handler_test.go
@@ -0,0 +1,227 @@
+package user
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+func issueToken(t *testing.T, userId uuid.UUID) string {
+ t.Helper()
+ tok := &authtoken.Token{Application: viper.GetString("server.application")}
+ access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId)
+ require.NoError(t, err)
+ return access
+}
+
+func newUserRouter(t *testing.T) *gin.Engine {
+ t.Helper()
+ r := gin.New()
+ ApiHandler(r.Group("/user"))
+ return r
+}
+
+func getWithBearer(t *testing.T, r *gin.Engine, path, token string) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func patchWithBearer(t *testing.T, r *gin.Engine, path, token string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func patchJSON(t *testing.T, r *gin.Engine, path string, body any) *httptest.ResponseRecorder {
+ t.Helper()
+ b, _ := json.Marshal(body)
+ req := httptest.NewRequest(http.MethodPatch, path, bytes.NewBuffer(b))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+func getRequest(t *testing.T, r *gin.Engine, path string) *httptest.ResponseRecorder {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ return w
+}
+
+// ---- Info ----
+
+func TestUserInfoHandlerSelf(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, "selfinfo@example.com", 10)
+ token := issueToken(t, user.UserId)
+ r := newUserRouter(t)
+
+ w := getWithBearer(t, r, "/user/info", token)
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ data, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.Equal(t, "selfinfo@example.com", data["email"])
+}
+
+func TestUserInfoHandlerOtherNotFound(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, "caller@example.com", 10)
+ token := issueToken(t, user.UserId)
+ r := newUserRouter(t)
+
+ w := getWithBearer(t, r, "/user/info/00000000-0000-0000-0000-000000000099", token)
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
+
+// ---- Update ----
+
+func TestUserUpdateHandlerEmptyNickname(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, "update@example.com", 10)
+ token := issueToken(t, user.UserId)
+ r := newUserRouter(t)
+
+ w := patchWithBearer(t, r, "/user/update", token, map[string]any{
+ "nickname": "",
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestUserUpdateHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, "update2@example.com", 10)
+ token := issueToken(t, user.UserId)
+ r := newUserRouter(t)
+
+ w := patchWithBearer(t, r, "/user/update", token, map[string]any{
+ "nickname": "New Nickname",
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+// ---- List (admin) ----
+
+func TestUserListHandlerRequiresOffset(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ admin := testutil.SeedUser(t, "admin@example.com", 40)
+ token := issueToken(t, admin.UserId)
+ r := newUserRouter(t)
+
+ w := getWithBearer(t, r, "/user/list", token)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestUserListHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ admin := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ for i := 0; i < 3; i++ {
+ testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ }
+ token := issueToken(t, admin.UserId)
+ r := newUserRouter(t)
+
+ w := getWithBearer(t, r, "/user/list?offset=0&limit=10", token)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ assert.NotNil(t, resp["data"])
+}
+
+func TestUserListHandlerLowPermission(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newUserRouter(t)
+
+ w := getWithBearer(t, r, "/user/list?offset=0", token)
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
+
+// ---- Other (info by user_id) ----
+
+func TestUserInfoHandlerOtherSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ caller := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ target := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(context.Background(), target.UserId, data.WithAllowPublic(true)))
+
+ token := issueToken(t, caller.UserId)
+ r := newUserRouter(t)
+
+ w := getWithBearer(t, r, "/user/info/"+target.UserId.String(), token)
+ assert.Equal(t, http.StatusOK, w.Code)
+ var resp map[string]any
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ d, ok := resp["data"].(map[string]any)
+ require.True(t, ok)
+ assert.Equal(t, target.Email, d["email"])
+}
+
+// ---- AdminUpdate ----
+
+func TestUserAdminUpdateHandlerSuccess(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ admin := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ target := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, admin.UserId)
+ r := newUserRouter(t)
+
+ w := patchWithBearer(t, r, "/user/update/"+target.UserId.String(), token, map[string]any{
+ "nickname": "Admin Set Nickname",
+ })
+ assert.Equal(t, http.StatusOK, w.Code)
+}
+
+func TestUserAdminUpdateHandlerInvalidUserId(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ admin := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ token := issueToken(t, admin.UserId)
+ r := newUserRouter(t)
+
+ w := patchWithBearer(t, r, "/user/update/not-a-uuid", token, map[string]any{
+ "nickname": "Test",
+ })
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestUserAdminUpdateHandlerLowPermission(t *testing.T) {
+ testutil.SetupWithAuth(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ target := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ token := issueToken(t, user.UserId)
+ r := newUserRouter(t)
+
+ w := patchWithBearer(t, r, "/user/update/"+target.UserId.String(), token, map[string]any{
+ "nickname": "Hacked",
+ })
+ assert.Equal(t, http.StatusForbidden, w.Code)
+}
+
diff --git a/charts/.gitkeep b/charts/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/client/cms/.gitignore b/client/cms/.gitignore
deleted file mode 100644
index 668095b..0000000
--- a/client/cms/.gitignore
+++ /dev/null
@@ -1,26 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-
-.direnv
diff --git a/client/cms/components.json b/client/cms/components.json
deleted file mode 100644
index 2b0833f..0000000
--- a/client/cms/components.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": false,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "src/index.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "iconLibrary": "lucide",
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "registries": {}
-}
diff --git a/client/cms/eslint.config.js b/client/cms/eslint.config.js
deleted file mode 100644
index 75e958b..0000000
--- a/client/cms/eslint.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import antfu from '@antfu/eslint-config';
-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/**/*'],
- react: true,
- stylistic: {
- semi: true,
- quotes: 'single',
- indent: 2,
- },
- typescript: {
- tsconfigPath: 'tsconfig.json',
- },
-}, ...pluginQuery.configs['flat/recommended']);
diff --git a/client/cms/index.html b/client/cms/index.html
deleted file mode 100644
index a16cb6b..0000000
--- a/client/cms/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- client
-
-
-
-
-
-
diff --git a/client/cms/package.json b/client/cms/package.json
deleted file mode 100644
index 266c7ff..0000000
--- a/client/cms/package.json
+++ /dev/null
@@ -1,101 +0,0 @@
-{
- "name": "client",
- "type": "module",
- "version": "0.0.0",
- "private": true,
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "@dnd-kit/core": "^6.3.1",
- "@dnd-kit/modifiers": "^9.0.0",
- "@dnd-kit/sortable": "^10.0.0",
- "@dnd-kit/utilities": "^3.2.2",
- "@hookform/resolvers": "^5.2.2",
- "@marsidev/react-turnstile": "^1.4.0",
- "@radix-ui/react-avatar": "^1.1.11",
- "@radix-ui/react-checkbox": "^1.3.3",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-dropdown-menu": "^2.1.16",
- "@radix-ui/react-label": "^2.1.8",
- "@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.8",
- "@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-tabs": "^1.1.13",
- "@radix-ui/react-toggle": "^1.1.10",
- "@radix-ui/react-toggle-group": "^1.1.11",
- "@radix-ui/react-tooltip": "^1.2.8",
- "@tabler/icons-react": "^3.36.0",
- "@tailwindcss/vite": "^4.1.18",
- "@tanstack/react-form": "^1.27.7",
- "@tanstack/react-query": "^5.90.12",
- "@tanstack/react-router": "^1.141.6",
- "@tanstack/react-router-devtools": "^1.141.6",
- "@tanstack/react-table": "^8.21.3",
- "@tanstack/zod-adapter": "^1.143.4",
- "@tanstack/zod-form-adapter": "^0.42.1",
- "@uiw/react-md-editor": "^4.0.11",
- "axios": "^1.13.2",
- "base-64": "^1.0.0",
- "buffer": "^6.0.3",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "culori": "^4.0.2",
- "immer": "^11.1.0",
- "lodash-es": "^4.17.22",
- "lucide-react": "^0.562.0",
- "next-themes": "^0.4.6",
- "qrcode": "^1.5.4",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "react-hook-form": "^7.69.0",
- "react-markdown": "^10.1.0",
- "recharts": "2.15.4",
- "sonner": "^2.0.7",
- "tailwind-merge": "^3.4.0",
- "tailwindcss": "^4.1.18",
- "utf8": "^3.0.0",
- "vaul": "^1.1.2",
- "zod": "^4.2.1",
- "zustand": "^5.0.9"
- },
- "devDependencies": {
- "@antfu/eslint-config": "^6.7.1",
- "@eslint-react/eslint-plugin": "^2.3.13",
- "@eslint/js": "^9.39.1",
- "@tailwindcss/typography": "^0.5.19",
- "@tanstack/eslint-plugin-query": "^5.91.2",
- "@tanstack/router-plugin": "^1.141.7",
- "@types/base-64": "^1.0.2",
- "@types/culori": "^4.0.1",
- "@types/lodash-es": "^4.17.12",
- "@types/node": "^25.0.3",
- "@types/qrcode": "^1.5.6",
- "@types/react": "^19.2.5",
- "@types/react-dom": "^19.2.3",
- "@types/utf8": "^3.0.3",
- "@vitejs/plugin-react": "^5.1.1",
- "eslint": "^9.39.1",
- "eslint-plugin-react-hooks": "^7.0.1",
- "eslint-plugin-react-refresh": "^0.4.26",
- "globals": "^16.5.0",
- "lint-staged": "^16.2.7",
- "simple-git-hooks": "^2.13.1",
- "tw-animate-css": "^1.4.0",
- "type-fest": "^5.4.1",
- "typescript": "~5.9.3",
- "typescript-eslint": "^8.46.4",
- "vite": "^7.2.4",
- "vite-plugin-svgr": "^4.5.0"
- },
- "simple-git-hooks": {
- "pre-commit": "bun run lint-staged"
- },
- "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
deleted file mode 100644
index 7bbc249..0000000
--- a/client/cms/pnpm-lock.yaml
+++ /dev/null
@@ -1,8472 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-importers:
-
- .:
- dependencies:
- '@dnd-kit/core':
- specifier: ^6.3.1
- version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@dnd-kit/modifiers':
- specifier: ^9.0.0
- version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
- '@dnd-kit/sortable':
- specifier: ^10.0.0
- version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
- '@dnd-kit/utilities':
- specifier: ^3.2.2
- version: 3.2.2(react@19.2.3)
- '@hookform/resolvers':
- specifier: ^5.2.2
- version: 5.2.2(react-hook-form@7.71.1(react@19.2.3))
- '@marsidev/react-turnstile':
- specifier: ^1.4.0
- version: 1.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@radix-ui/react-avatar':
- specifier: ^1.1.11
- version: 1.1.11(@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-checkbox':
- specifier: ^1.3.3
- version: 1.3.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-dialog':
- specifier: ^1.1.15
- version: 1.1.15(@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-dropdown-menu':
- specifier: ^2.1.16
- version: 2.1.16(@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-label':
- specifier: ^2.1.8
- version: 2.1.8(@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-select':
- specifier: ^2.2.6
- version: 2.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-separator':
- specifier: ^1.1.8
- version: 1.1.8(@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-slot':
- specifier: ^1.2.4
- version: 1.2.4(@types/react@19.2.8)(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)
- '@radix-ui/react-toggle':
- specifier: ^1.1.10
- version: 1.1.10(@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-toggle-group':
- specifier: ^1.1.11
- version: 1.1.11(@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-tooltip':
- specifier: ^1.2.8
- version: 1.2.8(@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)
- '@tabler/icons-react':
- specifier: ^3.36.0
- version: 3.36.1(react@19.2.3)
- '@tailwindcss/vite':
- specifier: ^4.1.18
- version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
- '@tanstack/react-form':
- specifier: ^1.27.7
- version: 1.27.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/react-query':
- specifier: ^5.90.12
- version: 5.90.19(react@19.2.3)
- '@tanstack/react-router':
- specifier: ^1.141.6
- version: 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/react-router-devtools':
- specifier: ^1.141.6
- version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/react-table':
- specifier: ^8.21.3
- version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/zod-adapter':
- specifier: ^1.143.4
- version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.5)
- '@tanstack/zod-form-adapter':
- specifier: ^0.42.1
- version: 0.42.1(zod@4.3.5)
- '@uiw/react-md-editor':
- specifier: ^4.0.11
- version: 4.0.11(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- axios:
- specifier: ^1.13.2
- version: 1.13.2
- base-64:
- specifier: ^1.0.0
- version: 1.0.0
- buffer:
- specifier: ^6.0.3
- version: 6.0.3
- class-variance-authority:
- specifier: ^0.7.1
- version: 0.7.1
- clsx:
- specifier: ^2.1.1
- version: 2.1.1
- culori:
- specifier: ^4.0.2
- version: 4.0.2
- immer:
- specifier: ^11.1.0
- version: 11.1.3
- lodash-es:
- specifier: ^4.17.22
- version: 4.17.22
- lucide-react:
- specifier: ^0.562.0
- version: 0.562.0(react@19.2.3)
- next-themes:
- specifier: ^0.4.6
- version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- qrcode:
- specifier: ^1.5.4
- version: 1.5.4
- react:
- specifier: ^19.2.0
- version: 19.2.3
- react-dom:
- specifier: ^19.2.0
- version: 19.2.3(react@19.2.3)
- react-hook-form:
- specifier: ^7.69.0
- version: 7.71.1(react@19.2.3)
- react-markdown:
- specifier: ^10.1.0
- version: 10.1.0(@types/react@19.2.8)(react@19.2.3)
- recharts:
- specifier: 2.15.4
- version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- sonner:
- specifier: ^2.0.7
- version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- tailwind-merge:
- specifier: ^3.4.0
- version: 3.4.0
- tailwindcss:
- specifier: ^4.1.18
- version: 4.1.18
- utf8:
- specifier: ^3.0.0
- version: 3.0.0
- vaul:
- specifier: ^1.1.2
- version: 1.1.2(@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)
- zod:
- specifier: ^4.2.1
- version: 4.3.5
- zustand:
- specifier: ^5.0.9
- version: 5.0.10(@types/react@19.2.8)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
- devDependencies:
- '@antfu/eslint-config':
- specifier: ^6.7.1
- version: 6.7.3(@eslint-react/eslint-plugin@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.27)(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eslint-plugin':
- specifier: ^2.3.13
- version: 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint/js':
- specifier: ^9.39.1
- version: 9.39.2
- '@tailwindcss/typography':
- specifier: ^0.5.19
- version: 0.5.19(tailwindcss@4.1.18)
- '@tanstack/eslint-plugin-query':
- specifier: ^5.91.2
- version: 5.91.3(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@tanstack/router-plugin':
- specifier: ^1.141.7
- version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
- '@types/base-64':
- specifier: ^1.0.2
- version: 1.0.2
- '@types/culori':
- specifier: ^4.0.1
- version: 4.0.1
- '@types/lodash-es':
- specifier: ^4.17.12
- version: 4.17.12
- '@types/node':
- specifier: ^25.0.3
- version: 25.0.9
- '@types/qrcode':
- specifier: ^1.5.6
- version: 1.5.6
- '@types/react':
- specifier: ^19.2.5
- version: 19.2.8
- '@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.8)
- '@types/utf8':
- specifier: ^3.0.3
- version: 3.0.3
- '@vitejs/plugin-react':
- specifier: ^5.1.1
- version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
- eslint:
- specifier: ^9.39.1
- version: 9.39.2(jiti@2.6.1)
- eslint-plugin-react-hooks:
- specifier: ^7.0.1
- version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-react-refresh:
- specifier: ^0.4.26
- version: 0.4.26(eslint@9.39.2(jiti@2.6.1))
- globals:
- specifier: ^16.5.0
- version: 16.5.0
- lint-staged:
- specifier: ^16.2.7
- version: 16.2.7
- simple-git-hooks:
- specifier: ^2.13.1
- version: 2.13.1
- tw-animate-css:
- specifier: ^1.4.0
- version: 1.4.0
- type-fest:
- specifier: ^5.4.1
- version: 5.4.1
- typescript:
- specifier: ~5.9.3
- version: 5.9.3
- typescript-eslint:
- specifier: ^8.46.4
- version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- vite:
- specifier: ^7.2.4
- version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
- vite-plugin-svgr:
- specifier: ^4.5.0
- version: 4.5.0(rollup@4.55.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
-
-packages:
-
- '@antfu/eslint-config@6.7.3':
- resolution: {integrity: sha512-0tYYzY59uLnxWgbP9xpuxpvodTcWDacj439kTAJZB3sn7O0BnPfVxTnRvleGYaKCEALBZkzdC/wCho9FD7ICLw==}
- hasBin: true
- peerDependencies:
- '@eslint-react/eslint-plugin': ^2.0.1
- '@next/eslint-plugin-next': '>=15.0.0'
- '@prettier/plugin-xml': ^3.4.1
- '@unocss/eslint-plugin': '>=0.50.0'
- astro-eslint-parser: ^1.0.2
- eslint: ^9.10.0
- eslint-plugin-astro: ^1.2.0
- eslint-plugin-format: '>=0.1.0'
- eslint-plugin-jsx-a11y: '>=6.10.2'
- eslint-plugin-react-hooks: ^7.0.0
- eslint-plugin-react-refresh: ^0.4.19
- eslint-plugin-solid: ^0.14.3
- eslint-plugin-svelte: '>=2.35.1'
- eslint-plugin-vuejs-accessibility: ^2.4.1
- prettier-plugin-astro: ^0.14.0
- prettier-plugin-slidev: ^1.0.5
- svelte-eslint-parser: '>=0.37.0'
- peerDependenciesMeta:
- '@eslint-react/eslint-plugin':
- optional: true
- '@next/eslint-plugin-next':
- optional: true
- '@prettier/plugin-xml':
- optional: true
- '@unocss/eslint-plugin':
- optional: true
- astro-eslint-parser:
- optional: true
- eslint-plugin-astro:
- optional: true
- eslint-plugin-format:
- optional: true
- eslint-plugin-jsx-a11y:
- optional: true
- eslint-plugin-react-hooks:
- optional: true
- eslint-plugin-react-refresh:
- optional: true
- eslint-plugin-solid:
- optional: true
- eslint-plugin-svelte:
- optional: true
- eslint-plugin-vuejs-accessibility:
- optional: true
- prettier-plugin-astro:
- optional: true
- prettier-plugin-slidev:
- optional: true
- svelte-eslint-parser:
- optional: true
-
- '@antfu/install-pkg@1.1.0':
- resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
-
- '@babel/code-frame@7.28.6':
- resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
- engines: {node: '>=6.9.0'}
-
- '@babel/compat-data@7.28.6':
- resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==}
- engines: {node: '>=6.9.0'}
-
- '@babel/core@7.28.6':
- resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==}
- engines: {node: '>=6.9.0'}
-
- '@babel/generator@7.28.6':
- resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-compilation-targets@7.28.6':
- resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-globals@7.28.0':
- resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-module-imports@7.28.6':
- resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-module-transforms@7.28.6':
- resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0
-
- '@babel/helper-plugin-utils@7.28.6':
- resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-string-parser@7.27.1':
- resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-validator-identifier@7.28.5':
- resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helper-validator-option@7.27.1':
- resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
- engines: {node: '>=6.9.0'}
-
- '@babel/helpers@7.28.6':
- resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
- engines: {node: '>=6.9.0'}
-
- '@babel/parser@7.28.6':
- resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==}
- engines: {node: '>=6.0.0'}
- hasBin: true
-
- '@babel/plugin-syntax-jsx@7.28.6':
- resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@babel/plugin-syntax-typescript@7.28.6':
- resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@babel/plugin-transform-react-jsx-self@7.27.1':
- resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@babel/plugin-transform-react-jsx-source@7.27.1':
- resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@babel/runtime@7.28.6':
- resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
- engines: {node: '>=6.9.0'}
-
- '@babel/template@7.28.6':
- resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
- engines: {node: '>=6.9.0'}
-
- '@babel/traverse@7.28.6':
- resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==}
- engines: {node: '>=6.9.0'}
-
- '@babel/types@7.28.6':
- resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
- engines: {node: '>=6.9.0'}
-
- '@clack/core@0.5.0':
- resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
-
- '@clack/prompts@0.11.0':
- resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
-
- '@dnd-kit/accessibility@3.1.1':
- resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
- peerDependencies:
- react: '>=16.8.0'
-
- '@dnd-kit/core@6.3.1':
- resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
-
- '@dnd-kit/modifiers@9.0.0':
- resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==}
- peerDependencies:
- '@dnd-kit/core': ^6.3.0
- react: '>=16.8.0'
-
- '@dnd-kit/sortable@10.0.0':
- resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
- peerDependencies:
- '@dnd-kit/core': ^6.3.0
- react: '>=16.8.0'
-
- '@dnd-kit/utilities@3.2.2':
- resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
- peerDependencies:
- react: '>=16.8.0'
-
- '@es-joy/jsdoccomment@0.78.0':
- resolution: {integrity: sha512-rQkU5u8hNAq2NVRzHnIUUvR6arbO0b6AOlvpTNS48CkiKSn/xtNfOzBK23JE4SiW89DgvU7GtxLVgV4Vn2HBAw==}
- engines: {node: '>=20.11.0'}
-
- '@es-joy/resolve.exports@1.2.0':
- resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==}
- engines: {node: '>=10'}
-
- '@esbuild/aix-ppc64@0.27.2':
- resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
- engines: {node: '>=18'}
- cpu: [ppc64]
- os: [aix]
-
- '@esbuild/android-arm64@0.27.2':
- resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [android]
-
- '@esbuild/android-arm@0.27.2':
- resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [android]
-
- '@esbuild/android-x64@0.27.2':
- resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [android]
-
- '@esbuild/darwin-arm64@0.27.2':
- resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [darwin]
-
- '@esbuild/darwin-x64@0.27.2':
- resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [darwin]
-
- '@esbuild/freebsd-arm64@0.27.2':
- resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [freebsd]
-
- '@esbuild/freebsd-x64@0.27.2':
- resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [freebsd]
-
- '@esbuild/linux-arm64@0.27.2':
- resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [linux]
-
- '@esbuild/linux-arm@0.27.2':
- resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [linux]
-
- '@esbuild/linux-ia32@0.27.2':
- resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
- engines: {node: '>=18'}
- cpu: [ia32]
- os: [linux]
-
- '@esbuild/linux-loong64@0.27.2':
- resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
- engines: {node: '>=18'}
- cpu: [loong64]
- os: [linux]
-
- '@esbuild/linux-mips64el@0.27.2':
- resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
- engines: {node: '>=18'}
- cpu: [mips64el]
- os: [linux]
-
- '@esbuild/linux-ppc64@0.27.2':
- resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
- engines: {node: '>=18'}
- cpu: [ppc64]
- os: [linux]
-
- '@esbuild/linux-riscv64@0.27.2':
- resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
- engines: {node: '>=18'}
- cpu: [riscv64]
- os: [linux]
-
- '@esbuild/linux-s390x@0.27.2':
- resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
- engines: {node: '>=18'}
- cpu: [s390x]
- os: [linux]
-
- '@esbuild/linux-x64@0.27.2':
- resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [linux]
-
- '@esbuild/netbsd-arm64@0.27.2':
- resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [netbsd]
-
- '@esbuild/netbsd-x64@0.27.2':
- resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [netbsd]
-
- '@esbuild/openbsd-arm64@0.27.2':
- resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [openbsd]
-
- '@esbuild/openbsd-x64@0.27.2':
- resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [openbsd]
-
- '@esbuild/openharmony-arm64@0.27.2':
- resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [openharmony]
-
- '@esbuild/sunos-x64@0.27.2':
- resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [sunos]
-
- '@esbuild/win32-arm64@0.27.2':
- resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [win32]
-
- '@esbuild/win32-ia32@0.27.2':
- resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
- engines: {node: '>=18'}
- cpu: [ia32]
- os: [win32]
-
- '@esbuild/win32-x64@0.27.2':
- resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [win32]
-
- '@eslint-community/eslint-plugin-eslint-comments@4.6.0':
- resolution: {integrity: sha512-2EX2bBQq1ez++xz2o9tEeEQkyvfieWgUFMH4rtJJri2q0Azvhja3hZGXsjPXs31R4fQkZDtWzNDDK2zQn5UE5g==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
-
- '@eslint-community/eslint-utils@4.9.1':
- resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-
- '@eslint-community/regexpp@4.12.2':
- resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
- engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
- '@eslint-react/ast@2.7.2':
- resolution: {integrity: sha512-RB8AVNjboN6/md9Da4rUG4WqxLT+DqUR+qXIR6iAD0+xxp6Dtihu541+lKLZ3GCstunbBcDwu7gdhSbz+BHSuQ==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@eslint-react/core@2.7.2':
- resolution: {integrity: sha512-QOYh8OWwUGMYLhuvb8WcmoS2jYXb0SJbpX+Ozk+Ht2G9XGRAahl+8PDy/o2l2lLnFXv5JQGfLrN+m2WPTi104g==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@eslint-react/eff@2.7.2':
- resolution: {integrity: sha512-AzQGbidoI8g8izka/1H9xCKW56NR7xWGGPMccBCUZwbCoJZ4wyRKcE10E7ot7LwBv5kBoUQp3GJ9UXCcg/Er0w==}
- engines: {node: '>=20.19.0'}
-
- '@eslint-react/eslint-plugin@2.7.2':
- resolution: {integrity: sha512-h9T5cc2TxsKMv/8iO63KKamXyJjHHAmeG2MJVjeIm4FaZdsX0/2Bx254B3Fa8IDqQi4X81AMyJ8ohtbxsn6pOw==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@eslint-react/shared@2.7.2':
- resolution: {integrity: sha512-U1H3dLaTj7kvEbyJyJEgn6xX3BmrCH1f9f+tg9gLWlN7askgWT5NF56wfX1l+jtwiEAZD/78W1TfICKkMnZDxQ==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@eslint-react/var@2.7.2':
- resolution: {integrity: sha512-sPnXmikpzmAdIWh6lqqKm4Bu0ypKTCAQ7WxGuR5ejxtrA/HjQQuKMBIyPkBdjHWlF9ADdh9pKuo1j2RQwUWiqA==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@eslint/compat@1.4.1':
- resolution: {integrity: sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.40 || 9
- peerDependenciesMeta:
- eslint:
- optional: true
-
- '@eslint/config-array@0.21.1':
- resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/config-helpers@0.4.2':
- resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/core@0.17.0':
- resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/eslintrc@3.3.3':
- resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/js@9.39.2':
- resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/markdown@7.5.1':
- resolution: {integrity: sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/object-schema@2.1.7':
- resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/plugin-kit@0.4.1':
- resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@floating-ui/core@1.7.3':
- resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
-
- '@floating-ui/dom@1.7.4':
- resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
-
- '@floating-ui/react-dom@2.1.6':
- resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
-
- '@floating-ui/utils@0.2.10':
- resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
-
- '@hookform/resolvers@5.2.2':
- resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
- peerDependencies:
- react-hook-form: ^7.55.0
-
- '@humanfs/core@0.19.1':
- resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
- engines: {node: '>=18.18.0'}
-
- '@humanfs/node@0.16.7':
- resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
- engines: {node: '>=18.18.0'}
-
- '@humanwhocodes/module-importer@1.0.1':
- resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
- engines: {node: '>=12.22'}
-
- '@humanwhocodes/retry@0.4.3':
- resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
- engines: {node: '>=18.18'}
-
- '@jridgewell/gen-mapping@0.3.13':
- resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
-
- '@jridgewell/remapping@2.3.5':
- resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
-
- '@jridgewell/resolve-uri@3.1.2':
- resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
- engines: {node: '>=6.0.0'}
-
- '@jridgewell/sourcemap-codec@1.5.5':
- resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
-
- '@jridgewell/trace-mapping@0.3.31':
- resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
-
- '@marsidev/react-turnstile@1.4.1':
- resolution: {integrity: sha512-1jE0IjvB8z+q1NFRs3149gXzXwIzXQWqQjn9fmAr13BiE3RYLWck5Me6flHYE90shW5L12Jkm6R1peS1OnA9oQ==}
- peerDependencies:
- react: ^17.0.2 || ^18.0.0 || ^19.0
- react-dom: ^17.0.2 || ^18.0.0 || ^19.0
-
- '@pkgr/core@0.2.9':
- resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
- engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
-
- '@radix-ui/number@1.1.1':
- resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
-
- '@radix-ui/primitive@1.1.3':
- resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
-
- '@radix-ui/react-arrow@1.1.7':
- resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
- 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-avatar@1.1.11':
- resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
- 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-checkbox@1.3.3':
- resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
- 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-collection@1.1.7':
- resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
- 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-compose-refs@1.1.2':
- resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-context@1.1.2':
- resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-context@1.1.3':
- resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-dialog@1.1.15':
- resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
- 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-direction@1.1.1':
- resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-dismissable-layer@1.1.11':
- resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
- 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-dropdown-menu@2.1.16':
- resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
- 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-focus-guards@1.1.3':
- resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-focus-scope@1.1.7':
- resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
- 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-id@1.1.1':
- resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-label@2.1.8':
- resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
- 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-menu@2.1.16':
- resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
- 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-popper@1.2.8':
- resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
- 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-portal@1.1.9':
- resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
- 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-presence@1.1.5':
- resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
- 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-primitive@2.1.3':
- resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
- 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-primitive@2.1.4':
- resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
- 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-roving-focus@1.1.11':
- resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
- 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-select@2.2.6':
- resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
- 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-separator@1.1.8':
- resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
- 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-slot@1.2.3':
- resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-slot@1.2.4':
- resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-tabs@1.1.13':
- resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
- 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-toggle-group@1.1.11':
- resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
- 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-toggle@1.1.10':
- resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==}
- 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-tooltip@1.2.8':
- resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
- 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-use-callback-ref@1.1.1':
- resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-controllable-state@1.2.2':
- resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-effect-event@0.0.2':
- resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-escape-keydown@1.1.1':
- resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-is-hydrated@0.1.0':
- resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-layout-effect@1.1.1':
- resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-previous@1.1.1':
- resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-rect@1.1.1':
- resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-use-size@1.1.1':
- resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@radix-ui/react-visually-hidden@1.2.3':
- resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
- 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/rect@1.1.1':
- resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
-
- '@rolldown/pluginutils@1.0.0-beta.53':
- resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
-
- '@rollup/pluginutils@5.3.0':
- resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
- engines: {node: '>=14.0.0'}
- peerDependencies:
- rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
- peerDependenciesMeta:
- rollup:
- optional: true
-
- '@rollup/rollup-android-arm-eabi@4.55.2':
- resolution: {integrity: sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==}
- cpu: [arm]
- os: [android]
-
- '@rollup/rollup-android-arm64@4.55.2':
- resolution: {integrity: sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==}
- cpu: [arm64]
- os: [android]
-
- '@rollup/rollup-darwin-arm64@4.55.2':
- resolution: {integrity: sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==}
- cpu: [arm64]
- os: [darwin]
-
- '@rollup/rollup-darwin-x64@4.55.2':
- resolution: {integrity: sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==}
- cpu: [x64]
- os: [darwin]
-
- '@rollup/rollup-freebsd-arm64@4.55.2':
- resolution: {integrity: sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==}
- cpu: [arm64]
- os: [freebsd]
-
- '@rollup/rollup-freebsd-x64@4.55.2':
- resolution: {integrity: sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==}
- cpu: [x64]
- os: [freebsd]
-
- '@rollup/rollup-linux-arm-gnueabihf@4.55.2':
- 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==}
- cpu: [x64]
- os: [openbsd]
-
- '@rollup/rollup-openharmony-arm64@4.55.2':
- resolution: {integrity: sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==}
- cpu: [arm64]
- os: [openharmony]
-
- '@rollup/rollup-win32-arm64-msvc@4.55.2':
- resolution: {integrity: sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==}
- cpu: [arm64]
- os: [win32]
-
- '@rollup/rollup-win32-ia32-msvc@4.55.2':
- resolution: {integrity: sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==}
- cpu: [ia32]
- os: [win32]
-
- '@rollup/rollup-win32-x64-gnu@4.55.2':
- resolution: {integrity: sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==}
- cpu: [x64]
- os: [win32]
-
- '@rollup/rollup-win32-x64-msvc@4.55.2':
- resolution: {integrity: sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==}
- cpu: [x64]
- os: [win32]
-
- '@sindresorhus/base62@1.0.0':
- resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==}
- engines: {node: '>=18'}
-
- '@standard-schema/utils@0.3.0':
- resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
-
- '@stylistic/eslint-plugin@5.7.0':
- resolution: {integrity: sha512-PsSugIf9ip1H/mWKj4bi/BlEoerxXAda9ByRFsYuwsmr6af9NxJL0AaiNXs8Le7R21QR5KMiD/KdxZZ71LjAxQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: '>=9.0.0'
-
- '@svgr/babel-plugin-add-jsx-attribute@8.0.0':
- resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-remove-jsx-attribute@8.0.0':
- resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0':
- resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0':
- resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-svg-dynamic-title@8.0.0':
- resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-svg-em-dimensions@8.0.0':
- resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-transform-react-native-svg@8.1.0':
- resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-plugin-transform-svg-component@8.0.0':
- resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==}
- engines: {node: '>=12'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/babel-preset@8.1.0':
- resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==}
- engines: {node: '>=14'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@svgr/core@8.1.0':
- resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
- engines: {node: '>=14'}
-
- '@svgr/hast-util-to-babel-ast@8.0.0':
- resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==}
- engines: {node: '>=14'}
-
- '@svgr/plugin-jsx@8.1.0':
- resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==}
- engines: {node: '>=14'}
- peerDependencies:
- '@svgr/core': '*'
-
- '@tabler/icons-react@3.36.1':
- resolution: {integrity: sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==}
- peerDependencies:
- react: '>= 16'
-
- '@tabler/icons@3.36.1':
- resolution: {integrity: sha512-f4Jg3Fof/Vru5ioix/UO4GX+sdDsF9wQo47FbtvG+utIYYVQ/QVAC0QYgcBbAjQGfbdOh2CCf0BgiFOF9Ixtjw==}
-
- '@tailwindcss/node@4.1.18':
- resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
-
- '@tailwindcss/oxide-android-arm64@4.1.18':
- resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [android]
-
- '@tailwindcss/oxide-darwin-arm64@4.1.18':
- resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [darwin]
-
- '@tailwindcss/oxide-darwin-x64@4.1.18':
- resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [darwin]
-
- '@tailwindcss/oxide-freebsd-x64@4.1.18':
- resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [freebsd]
-
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
- resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
- engines: {node: '>= 10'}
- cpu: [arm]
- os: [linux]
-
- '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
- resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
- 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==}
- engines: {node: '>=14.0.0'}
- cpu: [wasm32]
- bundledDependencies:
- - '@napi-rs/wasm-runtime'
- - '@emnapi/core'
- - '@emnapi/runtime'
- - '@tybys/wasm-util'
- - '@emnapi/wasi-threads'
- - tslib
-
- '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
- resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [win32]
-
- '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
- resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [win32]
-
- '@tailwindcss/oxide@4.1.18':
- resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
- engines: {node: '>= 10'}
-
- '@tailwindcss/typography@0.5.19':
- resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
- peerDependencies:
- tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
-
- '@tailwindcss/vite@4.1.18':
- resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
- peerDependencies:
- vite: ^5.2.0 || ^6 || ^7
-
- '@tanstack/devtools-event-client@0.4.0':
- resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==}
- engines: {node: '>=18'}
-
- '@tanstack/eslint-plugin-query@5.91.3':
- resolution: {integrity: sha512-5GMGZMYFK9dOvjpdedjJs4hU40EdPuO2AjzObQzP7eOSsikunCfrXaU3oNGXSsvoU9ve1Z1xQZZuDyPi0C1M7Q==}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
-
- '@tanstack/form-core@0.42.1':
- resolution: {integrity: sha512-jTU0jyHqFceujdtPNv3jPVej1dTqBwa8TYdIyWB5BCwRVUBZEp1PiYEBkC9r92xu5fMpBiKc+JKud3eeVjuMiA==}
-
- '@tanstack/form-core@1.27.7':
- resolution: {integrity: sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==}
-
- '@tanstack/history@1.153.2':
- resolution: {integrity: sha512-TVa0Wju5w6JZGq/S74Q7TQNtKXDatJaB4NYrhMZVU9ETlkgpr35NhDfOzsCJ93P0KCo1ZoDodlFp3c54/dLsyw==}
- engines: {node: '>=12'}
-
- '@tanstack/pacer-lite@0.1.1':
- resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==}
- engines: {node: '>=18'}
-
- '@tanstack/query-core@5.90.19':
- resolution: {integrity: sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==}
-
- '@tanstack/react-form@1.27.7':
- resolution: {integrity: sha512-xTg4qrUY0fuLaSnkATLZcK3BWlnwLp7IuAb6UTbZKngiDEvvDCNTvVvHgPlgef1O2qN4klZxInRyRY6oEkXZ2A==}
- peerDependencies:
- '@tanstack/react-start': '*'
- react: ^17.0.0 || ^18.0.0 || ^19.0.0
- peerDependenciesMeta:
- '@tanstack/react-start':
- optional: true
-
- '@tanstack/react-query@5.90.19':
- resolution: {integrity: sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==}
- peerDependencies:
- react: ^18 || ^19
-
- '@tanstack/react-router-devtools@1.153.2':
- resolution: {integrity: sha512-LCEuRIyrF0tNKCBspR+TQj13MQ7sTCE4QkkuKAOp30nSdWLxq53bltnGs9bj/V/PTD52JibuAOYyxB94ssWZUA==}
- engines: {node: '>=12'}
- peerDependencies:
- '@tanstack/react-router': ^1.153.2
- '@tanstack/router-core': ^1.153.2
- react: '>=18.0.0 || >=19.0.0'
- react-dom: '>=18.0.0 || >=19.0.0'
- peerDependenciesMeta:
- '@tanstack/router-core':
- optional: true
-
- '@tanstack/react-router@1.153.2':
- resolution: {integrity: sha512-fAXUBA2gZAId7h2eSHsRcgTeF8pioUz8V5rrQ+IrvA0a6IsxhbTSKLYyqUg4jRDkkcUKtM8StKtvbZCY+0IYWw==}
- engines: {node: '>=12'}
- peerDependencies:
- react: '>=18.0.0 || >=19.0.0'
- react-dom: '>=18.0.0 || >=19.0.0'
-
- '@tanstack/react-store@0.8.0':
- resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
- '@tanstack/react-table@8.21.3':
- resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
- engines: {node: '>=12'}
- peerDependencies:
- react: '>=16.8'
- react-dom: '>=16.8'
-
- '@tanstack/router-core@1.153.2':
- resolution: {integrity: sha512-WLaR+rSNW7bj9UCJQ3SKpuh6nZBZkpGnf2mpjn/uRB6joIQ3BU7aRdhb7w9Via/MP52iaHh5sd8NY3MaLpF2tQ==}
- engines: {node: '>=12'}
-
- '@tanstack/router-devtools-core@1.153.2':
- resolution: {integrity: sha512-53gFlnz2oUeGvRwu7hzi+jlqm5F5X1XwNniirCTjggsV5P+FVQ7YJ+gfMuN5MHonWmVCLd1QqGkl2nYRTGHeTg==}
- engines: {node: '>=12'}
- peerDependencies:
- '@tanstack/router-core': ^1.153.2
- csstype: ^3.0.10
- peerDependenciesMeta:
- csstype:
- optional: true
-
- '@tanstack/router-generator@1.153.2':
- resolution: {integrity: sha512-bEhmCtXq5vv3HukKq5zmTDBNDRqVllYxsHoWtqEvHv5hCb5xwKKfUMGemRoiQ96/wLFuGnA5DYkem2GZWcG3wg==}
- engines: {node: '>=12'}
-
- '@tanstack/router-plugin@1.153.2':
- resolution: {integrity: sha512-aMMc70ChM0wBYOToq39kTMKI2A0EKWpumiKTJyAwEglXf0raF48+26Fmv0gr9/5CLvD0g8ljllsskVDyzg8oDw==}
- engines: {node: '>=12'}
- peerDependencies:
- '@rsbuild/core': '>=1.0.2'
- '@tanstack/react-router': ^1.153.2
- vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
- vite-plugin-solid: ^2.11.10
- webpack: '>=5.92.0'
- peerDependenciesMeta:
- '@rsbuild/core':
- optional: true
- '@tanstack/react-router':
- optional: true
- vite:
- optional: true
- vite-plugin-solid:
- optional: true
- webpack:
- optional: true
-
- '@tanstack/router-utils@1.143.11':
- resolution: {integrity: sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA==}
- engines: {node: '>=12'}
-
- '@tanstack/store@0.7.7':
- resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
-
- '@tanstack/store@0.8.0':
- resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==}
-
- '@tanstack/table-core@8.21.3':
- resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
- engines: {node: '>=12'}
-
- '@tanstack/virtual-file-routes@1.145.4':
- resolution: {integrity: sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ==}
- engines: {node: '>=12'}
-
- '@tanstack/zod-adapter@1.153.2':
- resolution: {integrity: sha512-E3OPtB/QSRo9lN8wWbxjvjMfQI4MOKjTEZOrwZiHXXMzlOMYWqxg99eCPeHP8kcXKJ+LdIkf29sZ/0rC62I4aQ==}
- engines: {node: '>=12'}
- peerDependencies:
- '@tanstack/react-router': '>=1.43.2'
- zod: ^3.23.8
-
- '@tanstack/zod-form-adapter@0.42.1':
- resolution: {integrity: sha512-hPRM0lawVKP64yurW4c6KHZH6altMo2MQN14hfi+GMBTKjO9S7bW1x5LPZ5cayoJE3mBvdlahpSGT5rYZtSbXQ==}
- peerDependencies:
- zod: ^3.x
-
- '@types/babel__core@7.20.5':
- resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
-
- '@types/babel__generator@7.27.0':
- resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
-
- '@types/babel__template@7.4.4':
- resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
-
- '@types/babel__traverse@7.28.0':
- resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
-
- '@types/base-64@1.0.2':
- resolution: {integrity: sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw==}
-
- '@types/culori@4.0.1':
- resolution: {integrity: sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==}
-
- '@types/d3-array@3.2.2':
- resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
-
- '@types/d3-color@3.1.3':
- resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
-
- '@types/d3-ease@3.0.2':
- resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
-
- '@types/d3-interpolate@3.0.4':
- resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
-
- '@types/d3-path@3.1.1':
- resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
-
- '@types/d3-scale@4.0.9':
- resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
-
- '@types/d3-shape@3.1.8':
- resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
-
- '@types/d3-time@3.0.4':
- resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
-
- '@types/d3-timer@3.0.2':
- resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
-
- '@types/debug@4.1.12':
- resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
-
- '@types/estree-jsx@1.0.5':
- resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
-
- '@types/estree@1.0.8':
- resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
-
- '@types/hast@2.3.10':
- resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
-
- '@types/hast@3.0.4':
- resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
-
- '@types/json-schema@7.0.15':
- resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
-
- '@types/lodash-es@4.17.12':
- resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
-
- '@types/lodash@4.17.23':
- resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
-
- '@types/mdast@4.0.4':
- resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
-
- '@types/ms@2.1.0':
- resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
-
- '@types/node@25.0.9':
- resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==}
-
- '@types/prismjs@1.26.5':
- resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
-
- '@types/qrcode@1.5.6':
- resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
-
- '@types/react-dom@19.2.3':
- resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
- peerDependencies:
- '@types/react': ^19.2.0
-
- '@types/react@19.2.8':
- resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==}
-
- '@types/unist@2.0.11':
- resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
-
- '@types/unist@3.0.3':
- resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
-
- '@types/utf8@3.0.3':
- resolution: {integrity: sha512-+lqLGxWZsEe4Z6OrzBI7Ym4SMUTaMS5yOrHZ0/IL0bpIye1Qbs4PpobJL2mLDbftUXlPFZR7fu6d1yM+bHLX1w==}
-
- '@typescript-eslint/eslint-plugin@8.53.1':
- resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- '@typescript-eslint/parser': ^8.53.1
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/parser@8.53.1':
- resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/project-service@8.53.1':
- resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/scope-manager@8.53.1':
- resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/tsconfig-utils@8.53.1':
- resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/type-utils@8.53.1':
- resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/types@8.53.1':
- resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@typescript-eslint/typescript-estree@8.53.1':
- resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/utils@8.53.1':
- resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- '@typescript-eslint/visitor-keys@8.53.1':
- resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@uiw/copy-to-clipboard@1.0.19':
- resolution: {integrity: sha512-AYxzFUBkZrhtExb2QC0C4lFH2+BSx6JVId9iqeGHakBuosqiQHUQaNZCvIBeM97Ucp+nJ22flOh8FBT2pKRRAA==}
-
- '@uiw/react-markdown-preview@5.1.5':
- resolution: {integrity: sha512-DNOqx1a6gJR7Btt57zpGEKTfHRlb7rWbtctMRO2f82wWcuoJsxPBrM+JWebDdOD0LfD8oe2CQvW2ICQJKHQhZg==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
-
- '@uiw/react-md-editor@4.0.11':
- resolution: {integrity: sha512-F0OR5O1v54EkZYvJj3ew0I7UqLiPeU34hMAY4MdXS3hI86rruYi5DHVkG/VuvLkUZW7wIETM2QFtZ459gKIjQA==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
-
- '@ungap/structured-clone@1.3.0':
- resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
-
- '@vitejs/plugin-react@5.1.2':
- resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- peerDependencies:
- vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
-
- '@vitest/eslint-plugin@1.6.6':
- resolution: {integrity: sha512-bwgQxQWRtnTVzsUHK824tBmHzjV0iTx3tZaiQIYDjX3SA7TsQS8CuDVqxXrRY3FaOUMgbGavesCxI9MOfFLm7Q==}
- engines: {node: '>=18'}
- peerDependencies:
- eslint: '>=8.57.0'
- typescript: '>=5.0.0'
- vitest: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
- vitest:
- optional: true
-
- '@vue/compiler-core@3.5.27':
- resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==}
-
- '@vue/compiler-dom@3.5.27':
- resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==}
-
- '@vue/compiler-sfc@3.5.27':
- resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==}
-
- '@vue/compiler-ssr@3.5.27':
- resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==}
-
- '@vue/shared@3.5.27':
- resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==}
-
- acorn-jsx@5.3.2:
- resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
- peerDependencies:
- acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
-
- acorn@8.15.0:
- resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
- engines: {node: '>=0.4.0'}
- hasBin: true
-
- ajv@6.12.6:
- resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
-
- ansi-escapes@7.2.0:
- resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==}
- engines: {node: '>=18'}
-
- ansi-regex@5.0.1:
- resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
- engines: {node: '>=8'}
-
- ansi-regex@6.2.2:
- resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
- engines: {node: '>=12'}
-
- ansi-styles@4.3.0:
- resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
- engines: {node: '>=8'}
-
- ansi-styles@6.2.3:
- resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
- engines: {node: '>=12'}
-
- ansis@4.2.0:
- resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
- engines: {node: '>=14'}
-
- anymatch@3.1.3:
- resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
- engines: {node: '>= 8'}
-
- are-docs-informative@0.0.2:
- resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
- engines: {node: '>=14'}
-
- argparse@2.0.1:
- resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
-
- aria-hidden@1.2.6:
- resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
- engines: {node: '>=10'}
-
- ast-types@0.16.1:
- resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
- engines: {node: '>=4'}
-
- asynckit@0.4.0:
- resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
-
- axios@1.13.2:
- resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
-
- babel-dead-code-elimination@1.0.12:
- resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
-
- bail@2.0.2:
- resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
-
- balanced-match@1.0.2:
- resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
-
- base-64@1.0.0:
- resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
-
- base64-js@1.5.1:
- resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
-
- baseline-browser-mapping@2.9.15:
- resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==}
- hasBin: true
-
- bcp-47-match@2.0.3:
- resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
-
- binary-extensions@2.3.0:
- resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
- engines: {node: '>=8'}
-
- birecord@0.1.1:
- resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==}
-
- boolbase@1.0.0:
- resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
-
- brace-expansion@1.1.12:
- resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
-
- brace-expansion@2.0.2:
- resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
-
- braces@3.0.3:
- resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
- engines: {node: '>=8'}
-
- browserslist@4.28.1:
- resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
- buffer@6.0.3:
- resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
-
- builtin-modules@5.0.0:
- resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==}
- engines: {node: '>=18.20'}
-
- cac@6.7.14:
- resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
- engines: {node: '>=8'}
-
- call-bind-apply-helpers@1.0.2:
- resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
- engines: {node: '>= 0.4'}
-
- callsites@3.1.0:
- resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
- engines: {node: '>=6'}
-
- camelcase@5.3.1:
- resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
- engines: {node: '>=6'}
-
- camelcase@6.3.0:
- resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
- engines: {node: '>=10'}
-
- caniuse-lite@1.0.30001765:
- resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==}
-
- ccount@2.0.1:
- resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
-
- chalk@4.1.2:
- resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
- engines: {node: '>=10'}
-
- change-case@5.4.4:
- resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
-
- character-entities-html4@2.1.0:
- resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
-
- character-entities-legacy@3.0.0:
- resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
-
- character-entities@2.0.2:
- resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
-
- character-reference-invalid@2.0.1:
- resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
-
- chokidar@3.6.0:
- resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
- engines: {node: '>= 8.10.0'}
-
- ci-info@4.3.1:
- resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==}
- engines: {node: '>=8'}
-
- class-variance-authority@0.7.1:
- resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
-
- clean-regexp@1.0.0:
- resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
- engines: {node: '>=4'}
-
- cli-cursor@5.0.0:
- resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
- engines: {node: '>=18'}
-
- cli-truncate@5.1.1:
- resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==}
- engines: {node: '>=20'}
-
- cliui@6.0.0:
- resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
-
- clsx@2.1.1:
- resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
- engines: {node: '>=6'}
-
- color-convert@2.0.1:
- resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
- engines: {node: '>=7.0.0'}
-
- color-name@1.1.4:
- resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
-
- colorette@2.0.20:
- resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
-
- combined-stream@1.0.8:
- resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
- engines: {node: '>= 0.8'}
-
- comma-separated-tokens@2.0.3:
- resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
-
- commander@14.0.2:
- resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
- engines: {node: '>=20'}
-
- comment-parser@1.4.1:
- resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
- engines: {node: '>= 12.0.0'}
-
- comment-parser@1.4.4:
- resolution: {integrity: sha512-0D6qSQ5IkeRrGJFHRClzaMOenMeT0gErz3zIw3AprKMqhRN6LNU2jQOdkPG/FZ+8bCgXE1VidrgSzlBBDZRr8A==}
- engines: {node: '>= 12.0.0'}
-
- compare-versions@6.1.1:
- resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
-
- concat-map@0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
-
- confbox@0.1.8:
- resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
-
- confbox@0.2.2:
- resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
-
- convert-source-map@2.0.0:
- resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
-
- cookie-es@2.0.0:
- resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
-
- core-js-compat@3.47.0:
- resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==}
-
- cosmiconfig@8.3.6:
- resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
- engines: {node: '>=14'}
- peerDependencies:
- typescript: '>=4.9.5'
- peerDependenciesMeta:
- typescript:
- optional: true
-
- cross-spawn@7.0.6:
- resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
- engines: {node: '>= 8'}
-
- css-selector-parser@3.3.0:
- resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==}
-
- cssesc@3.0.0:
- resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
- engines: {node: '>=4'}
- hasBin: true
-
- csstype@3.2.3:
- resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
-
- culori@4.0.2:
- resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
- d3-array@3.2.4:
- resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
- engines: {node: '>=12'}
-
- d3-color@3.1.0:
- resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
- engines: {node: '>=12'}
-
- d3-ease@3.0.1:
- resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
- engines: {node: '>=12'}
-
- d3-format@3.1.2:
- resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
- engines: {node: '>=12'}
-
- d3-interpolate@3.0.1:
- resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
- engines: {node: '>=12'}
-
- d3-path@3.1.0:
- resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
- engines: {node: '>=12'}
-
- d3-scale@4.0.2:
- resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
- engines: {node: '>=12'}
-
- d3-shape@3.2.0:
- resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
- engines: {node: '>=12'}
-
- d3-time-format@4.1.0:
- resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
- engines: {node: '>=12'}
-
- d3-time@3.1.0:
- resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
- engines: {node: '>=12'}
-
- d3-timer@3.0.1:
- resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
- engines: {node: '>=12'}
-
- debug@4.4.3:
- resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
- engines: {node: '>=6.0'}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
-
- decamelize@1.2.0:
- resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
- engines: {node: '>=0.10.0'}
-
- decimal.js-light@2.5.1:
- resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
-
- decode-named-character-reference@1.3.0:
- resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
-
- deep-is@0.1.4:
- resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
-
- delayed-stream@1.0.0:
- resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
- engines: {node: '>=0.4.0'}
-
- dequal@2.0.3:
- resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
- engines: {node: '>=6'}
-
- detect-libc@2.1.2:
- resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
- engines: {node: '>=8'}
-
- detect-node-es@1.1.0:
- resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
-
- devlop@1.1.0:
- resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
-
- diff-sequences@27.5.1:
- resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==}
- engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
-
- diff@8.0.3:
- resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
- engines: {node: '>=0.3.1'}
-
- dijkstrajs@1.0.3:
- resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
-
- direction@2.0.1:
- resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
- hasBin: true
-
- dom-helpers@5.2.1:
- resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
-
- dot-case@3.0.4:
- resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
-
- dunder-proto@1.0.1:
- resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
- engines: {node: '>= 0.4'}
-
- electron-to-chromium@1.5.267:
- resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
-
- emoji-regex@10.6.0:
- resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
-
- emoji-regex@8.0.0:
- resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-
- empathic@2.0.0:
- resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
- engines: {node: '>=14'}
-
- enhanced-resolve@5.18.4:
- resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
- engines: {node: '>=10.13.0'}
-
- entities@4.5.0:
- resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
- engines: {node: '>=0.12'}
-
- entities@6.0.1:
- resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
- engines: {node: '>=0.12'}
-
- entities@7.0.0:
- resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
- engines: {node: '>=0.12'}
-
- environment@1.1.0:
- resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
- engines: {node: '>=18'}
-
- error-ex@1.3.4:
- resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
-
- es-define-property@1.0.1:
- resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
- engines: {node: '>= 0.4'}
-
- es-errors@1.3.0:
- resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
- engines: {node: '>= 0.4'}
-
- es-object-atoms@1.1.1:
- resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
- engines: {node: '>= 0.4'}
-
- es-set-tostringtag@2.1.0:
- resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
- engines: {node: '>= 0.4'}
-
- esbuild@0.27.2:
- resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
- engines: {node: '>=18'}
- hasBin: true
-
- escalade@3.2.0:
- resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
- engines: {node: '>=6'}
-
- escape-string-regexp@1.0.5:
- resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
- engines: {node: '>=0.8.0'}
-
- escape-string-regexp@4.0.0:
- resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
- engines: {node: '>=10'}
-
- escape-string-regexp@5.0.0:
- resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
- engines: {node: '>=12'}
-
- eslint-compat-utils@0.5.1:
- resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==}
- engines: {node: '>=12'}
- peerDependencies:
- eslint: '>=6.0.0'
-
- eslint-compat-utils@0.6.5:
- resolution: {integrity: sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ==}
- engines: {node: '>=12'}
- peerDependencies:
- eslint: '>=6.0.0'
-
- eslint-config-flat-gitignore@2.1.0:
- resolution: {integrity: sha512-cJzNJ7L+psWp5mXM7jBX+fjHtBvvh06RBlcweMhKD8jWqQw0G78hOW5tpVALGHGFPsBV+ot2H+pdDGJy6CV8pA==}
- peerDependencies:
- eslint: ^9.5.0
-
- eslint-flat-config-utils@2.1.4:
- resolution: {integrity: sha512-bEnmU5gqzS+4O+id9vrbP43vByjF+8KOs+QuuV4OlqAuXmnRW2zfI/Rza1fQvdihQ5h4DUo0NqFAiViD4mSrzQ==}
-
- eslint-json-compat-utils@0.2.1:
- resolution: {integrity: sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==}
- engines: {node: '>=12'}
- peerDependencies:
- '@eslint/json': '*'
- eslint: '*'
- jsonc-eslint-parser: ^2.4.0
- peerDependenciesMeta:
- '@eslint/json':
- optional: true
-
- eslint-merge-processors@2.0.0:
- resolution: {integrity: sha512-sUuhSf3IrJdGooquEUB5TNpGNpBoQccbnaLHsb1XkBLUPPqCNivCpY05ZcpCOiV9uHwO2yxXEWVczVclzMxYlA==}
- peerDependencies:
- eslint: '*'
-
- eslint-plugin-antfu@3.1.3:
- resolution: {integrity: sha512-Az1QuqQJ/c2efWCxVxF249u3D4AcAu1Y3VCGAlJm+x4cgnn1ybUAnCT5DWVcogeaWduQKeVw07YFydVTOF4xDw==}
- peerDependencies:
- eslint: '*'
-
- eslint-plugin-command@3.4.0:
- resolution: {integrity: sha512-EW4eg/a7TKEhG0s5IEti72kh3YOTlnhfFNuctq5WnB1fst37/IHTd5OkD+vnlRf3opTvUcSRihAateP6bT5ZcA==}
- peerDependencies:
- eslint: '*'
-
- eslint-plugin-es-x@7.8.0:
- resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==}
- engines: {node: ^14.18.0 || >=16.0.0}
- peerDependencies:
- eslint: '>=8'
-
- eslint-plugin-import-lite@0.4.0:
- resolution: {integrity: sha512-My0ReAg8WbHXYECIHVJkWB8UxrinZn3m72yonOYH6MFj40ZN1vHYQj16iq2Fd8Wrt/vRZJwDX2xm/BzDk1FzTg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: '>=9.0.0'
- typescript: '>=4.5'
- peerDependenciesMeta:
- typescript:
- optional: true
-
- eslint-plugin-jsdoc@61.7.1:
- resolution: {integrity: sha512-36DpldF95MlTX//n3/naULFVt8d1cV4jmSkx7ZKrE9ikkKHAgMLesuWp1SmwpVwAs5ndIM6abKd6PeOYZUgdWg==}
- engines: {node: '>=20.11.0'}
- peerDependencies:
- eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
-
- eslint-plugin-jsonc@2.21.0:
- resolution: {integrity: sha512-HttlxdNG5ly3YjP1cFMP62R4qKLxJURfBZo2gnMY+yQojZxkLyOpY1H1KRTKBmvQeSG9pIpSGEhDjE17vvYosg==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: '>=6.0.0'
-
- eslint-plugin-n@17.23.2:
- resolution: {integrity: sha512-RhWBeb7YVPmNa2eggvJooiuehdL76/bbfj/OJewyoGT80qn5PXdz8zMOTO6YHOsI7byPt7+Ighh/i/4a5/v7hw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: '>=8.23.0'
-
- eslint-plugin-no-only-tests@3.3.0:
- resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==}
- engines: {node: '>=5.0.0'}
-
- eslint-plugin-perfectionist@4.15.1:
- resolution: {integrity: sha512-MHF0cBoOG0XyBf7G0EAFCuJJu4I18wy0zAoT1OHfx2o6EOx1EFTIzr2HGeuZa1kDcusoX0xJ9V7oZmaeFd773Q==}
- engines: {node: ^18.0.0 || >=20.0.0}
- peerDependencies:
- eslint: '>=8.45.0'
-
- eslint-plugin-pnpm@1.5.0:
- resolution: {integrity: sha512-ayMo1GvrQ/sF/bz1aOAiH0jv9eAqU2Z+a1ycoWz/uFFK5NxQDq49BDKQtBumcOUBf2VHyiTW4a8u+6KVqoIWzQ==}
- peerDependencies:
- eslint: ^9.0.0
-
- eslint-plugin-react-dom@2.7.2:
- resolution: {integrity: sha512-Qzd4HAFwsxvOJoAycLIRxziOTJwEZ6EGAA6jEFFBSD1BbFVnDlozMvOLp9/+GrZW3cE0FGmAS6QXnjuMf0QYLQ==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- eslint-plugin-react-hooks-extra@2.7.2:
- resolution: {integrity: sha512-wcjQeBO1naCFPV47osw7nnK2p81eudCE2PhasKLtBV+GcAEi34jbt9QGULzQYueP+zd1aW53SmnVrTinY4DC6w==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- eslint-plugin-react-hooks@7.0.1:
- resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
- engines: {node: '>=18'}
- peerDependencies:
- eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
-
- eslint-plugin-react-naming-convention@2.7.2:
- resolution: {integrity: sha512-T+/RQFEda3AgCzBHguE3isLQetn8KUOZ14SnDBQSOZSWS/GjgQn+gmqHi3EVHX/sDdL+LsIUKRsRR6KmmYWMiw==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- eslint-plugin-react-refresh@0.4.26:
- resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==}
- peerDependencies:
- eslint: '>=8.40'
-
- eslint-plugin-react-web-api@2.7.2:
- resolution: {integrity: sha512-iA3D8jbwasMeeUfK8XucGkgrjQvZowCTi1+TzA43U7IFsWzyQWQpbN/I9B0BY/g6/JU9falC5b7qv6HB7P5JhA==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- eslint-plugin-react-x@2.7.2:
- resolution: {integrity: sha512-0NbYqJhc3tZQVluaFMVCOg6HEFarlNNXe+DHa/JrLAR0PVb9AtJGk8FBEDdxaUZO8ph0sAekUNLB7gymftj4Dw==}
- engines: {node: '>=20.19.0'}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- eslint-plugin-regexp@2.10.0:
- resolution: {integrity: sha512-ovzQT8ESVn5oOe5a7gIDPD5v9bCSjIFJu57sVPDqgPRXicQzOnYfFN21WoQBQF18vrhT5o7UMKFwJQVVjyJ0ng==}
- engines: {node: ^18 || >=20}
- peerDependencies:
- eslint: '>=8.44.0'
-
- eslint-plugin-toml@0.12.0:
- resolution: {integrity: sha512-+/wVObA9DVhwZB1nG83D2OAQRrcQZXy+drqUnFJKymqnmbnbfg/UPmEMCKrJNcEboUGxUjYrJlgy+/Y930mURQ==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: '>=6.0.0'
-
- eslint-plugin-unicorn@62.0.0:
- resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==}
- engines: {node: ^20.10.0 || >=21.0.0}
- peerDependencies:
- eslint: '>=9.38.0'
-
- eslint-plugin-unused-imports@4.3.0:
- resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==}
- peerDependencies:
- '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
- eslint: ^9.0.0 || ^8.0.0
- peerDependenciesMeta:
- '@typescript-eslint/eslint-plugin':
- optional: true
-
- eslint-plugin-vue@10.7.0:
- resolution: {integrity: sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
- '@typescript-eslint/parser': ^7.0.0 || ^8.0.0
- eslint: ^8.57.0 || ^9.0.0
- vue-eslint-parser: ^10.0.0
- peerDependenciesMeta:
- '@stylistic/eslint-plugin':
- optional: true
- '@typescript-eslint/parser':
- optional: true
-
- eslint-plugin-yml@1.19.1:
- resolution: {integrity: sha512-bYkOxyEiXh9WxUhVYPELdSHxGG5pOjCSeJOVkfdIyj6tuiHDxrES2WAW1dBxn3iaZQey57XflwLtCYRcNPOiOg==}
- engines: {node: ^14.17.0 || >=16.0.0}
- peerDependencies:
- eslint: '>=6.0.0'
-
- eslint-processor-vue-blocks@2.0.0:
- resolution: {integrity: sha512-u4W0CJwGoWY3bjXAuFpc/b6eK3NQEI8MoeW7ritKj3G3z/WtHrKjkqf+wk8mPEy5rlMGS+k6AZYOw2XBoN/02Q==}
- peerDependencies:
- '@vue/compiler-sfc': ^3.3.0
- eslint: '>=9.0.0'
-
- eslint-scope@8.4.0:
- resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- eslint-visitor-keys@3.4.3:
- resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
- eslint-visitor-keys@4.2.1:
- resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- eslint-visitor-keys@5.0.0:
- resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-
- eslint@9.39.2:
- resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- hasBin: true
- peerDependencies:
- jiti: '*'
- peerDependenciesMeta:
- jiti:
- optional: true
-
- espree@10.4.0:
- resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- espree@11.1.0:
- resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-
- espree@9.6.1:
- resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
- esprima@4.0.1:
- resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
- engines: {node: '>=4'}
- hasBin: true
-
- esquery@1.7.0:
- resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
- engines: {node: '>=0.10'}
-
- esrecurse@4.3.0:
- resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
- engines: {node: '>=4.0'}
-
- estraverse@5.3.0:
- resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
- engines: {node: '>=4.0'}
-
- estree-util-is-identifier-name@3.0.0:
- resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
-
- estree-walker@2.0.2:
- resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
-
- esutils@2.0.3:
- resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
- engines: {node: '>=0.10.0'}
-
- eventemitter3@4.0.7:
- resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
-
- eventemitter3@5.0.4:
- resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
-
- exsolve@1.0.8:
- resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
-
- extend@3.0.2:
- resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
-
- fast-deep-equal@3.1.3:
- resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
-
- fast-equals@5.4.0:
- resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
- engines: {node: '>=6.0.0'}
-
- fast-json-stable-stringify@2.1.0:
- resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
-
- fast-levenshtein@2.0.6:
- resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
-
- fault@2.0.1:
- resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
-
- fdir@6.5.0:
- resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- picomatch: ^3 || ^4
- peerDependenciesMeta:
- picomatch:
- optional: true
-
- file-entry-cache@8.0.0:
- resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
- engines: {node: '>=16.0.0'}
-
- fill-range@7.1.1:
- resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
- engines: {node: '>=8'}
-
- find-up-simple@1.0.1:
- resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
- engines: {node: '>=18'}
-
- find-up@4.1.0:
- resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
- engines: {node: '>=8'}
-
- find-up@5.0.0:
- resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
- engines: {node: '>=10'}
-
- flat-cache@4.0.1:
- resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
- engines: {node: '>=16'}
-
- flatted@3.3.3:
- resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
-
- follow-redirects@1.15.11:
- resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
- engines: {node: '>=4.0'}
- peerDependencies:
- debug: '*'
- peerDependenciesMeta:
- debug:
- optional: true
-
- form-data@4.0.5:
- resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
- engines: {node: '>= 6'}
-
- format@0.2.2:
- resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
- engines: {node: '>=0.4.x'}
-
- fsevents@2.3.3:
- resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
- engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
- os: [darwin]
-
- function-bind@1.1.2:
- resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-
- gensync@1.0.0-beta.2:
- resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
- engines: {node: '>=6.9.0'}
-
- get-caller-file@2.0.5:
- resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
- engines: {node: 6.* || 8.* || >= 10.*}
-
- get-east-asian-width@1.4.0:
- resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
- engines: {node: '>=18'}
-
- get-intrinsic@1.3.0:
- resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
- engines: {node: '>= 0.4'}
-
- get-nonce@1.0.1:
- resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
- engines: {node: '>=6'}
-
- get-proto@1.0.1:
- resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
- engines: {node: '>= 0.4'}
-
- get-tsconfig@4.13.0:
- resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
-
- github-slugger@2.0.0:
- resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
-
- glob-parent@5.1.2:
- resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
- engines: {node: '>= 6'}
-
- glob-parent@6.0.2:
- resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
- engines: {node: '>=10.13.0'}
-
- globals@14.0.0:
- resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
- engines: {node: '>=18'}
-
- globals@15.15.0:
- resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
- engines: {node: '>=18'}
-
- globals@16.5.0:
- resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
- engines: {node: '>=18'}
-
- globrex@0.1.2:
- resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
-
- goober@2.1.18:
- resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==}
- peerDependencies:
- csstype: ^3.0.10
-
- gopd@1.2.0:
- resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
- engines: {node: '>= 0.4'}
-
- graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
-
- graphemer@1.4.0:
- resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
-
- has-flag@4.0.0:
- resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
- engines: {node: '>=8'}
-
- has-symbols@1.1.0:
- resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
- engines: {node: '>= 0.4'}
-
- has-tostringtag@1.0.2:
- resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
- engines: {node: '>= 0.4'}
-
- hasown@2.0.2:
- resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
- engines: {node: '>= 0.4'}
-
- hast-util-from-html@2.0.3:
- resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
-
- hast-util-from-parse5@8.0.3:
- resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
-
- hast-util-has-property@3.0.0:
- resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
-
- hast-util-heading-rank@3.0.0:
- resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
-
- hast-util-is-element@3.0.0:
- resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
-
- hast-util-parse-selector@3.1.1:
- resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==}
-
- hast-util-parse-selector@4.0.0:
- resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
-
- hast-util-raw@9.1.0:
- resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
-
- hast-util-select@6.0.4:
- resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==}
-
- hast-util-to-html@9.0.5:
- resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
-
- hast-util-to-jsx-runtime@2.3.6:
- resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
-
- hast-util-to-parse5@8.0.1:
- resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
-
- hast-util-to-string@3.0.1:
- resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
-
- hast-util-whitespace@3.0.0:
- resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
-
- hastscript@7.2.0:
- resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==}
-
- hastscript@9.0.1:
- resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
-
- hermes-estree@0.25.1:
- resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
-
- hermes-parser@0.25.1:
- resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
-
- html-entities@2.6.0:
- resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
-
- html-url-attributes@3.0.1:
- resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
-
- html-void-elements@3.0.0:
- resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
-
- ieee754@1.2.1:
- resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
-
- ignore@5.3.2:
- resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
- engines: {node: '>= 4'}
-
- ignore@7.0.5:
- resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
- engines: {node: '>= 4'}
-
- immer@11.1.3:
- resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==}
-
- import-fresh@3.3.1:
- resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
- engines: {node: '>=6'}
-
- imurmurhash@0.1.4:
- resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
- engines: {node: '>=0.8.19'}
-
- indent-string@5.0.0:
- resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
- engines: {node: '>=12'}
-
- inline-style-parser@0.2.7:
- resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
-
- internmap@2.0.3:
- resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
- engines: {node: '>=12'}
-
- is-alphabetical@2.0.1:
- resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
-
- is-alphanumerical@2.0.1:
- resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
-
- is-arrayish@0.2.1:
- resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
-
- is-binary-path@2.1.0:
- resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
- engines: {node: '>=8'}
-
- is-builtin-module@5.0.0:
- resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==}
- engines: {node: '>=18.20'}
-
- is-decimal@2.0.1:
- resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
-
- is-extglob@2.1.1:
- resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
- engines: {node: '>=0.10.0'}
-
- is-fullwidth-code-point@3.0.0:
- resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
- engines: {node: '>=8'}
-
- is-fullwidth-code-point@5.1.0:
- resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==}
- engines: {node: '>=18'}
-
- is-glob@4.0.3:
- resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
- engines: {node: '>=0.10.0'}
-
- is-hexadecimal@2.0.1:
- resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
-
- is-immutable-type@5.0.1:
- resolution: {integrity: sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==}
- peerDependencies:
- eslint: '*'
- typescript: '>=4.7.4'
-
- is-number@7.0.0:
- resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
- engines: {node: '>=0.12.0'}
-
- is-plain-obj@4.1.0:
- resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
- engines: {node: '>=12'}
-
- isbot@5.1.33:
- resolution: {integrity: sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg==}
- engines: {node: '>=18'}
-
- isexe@2.0.0:
- resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
-
- jiti@2.6.1:
- resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
- hasBin: true
-
- js-tokens@4.0.0:
- resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
-
- js-yaml@4.1.1:
- resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
- hasBin: true
-
- jsdoc-type-pratt-parser@4.8.0:
- resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==}
- engines: {node: '>=12.0.0'}
-
- jsdoc-type-pratt-parser@7.0.0:
- resolution: {integrity: sha512-c7YbokssPOSHmqTbSAmTtnVgAVa/7lumWNYqomgd5KOMyPrRve2anx6lonfOsXEQacqF9FKVUj7bLg4vRSvdYA==}
- engines: {node: '>=20.0.0'}
-
- jsesc@3.1.0:
- resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
- engines: {node: '>=6'}
- hasBin: true
-
- json-buffer@3.0.1:
- resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
-
- json-parse-even-better-errors@2.3.1:
- resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
-
- json-schema-traverse@0.4.1:
- resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
-
- json-stable-stringify-without-jsonify@1.0.1:
- resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
-
- json5@2.2.3:
- resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
- engines: {node: '>=6'}
- hasBin: true
-
- jsonc-eslint-parser@2.4.2:
- resolution: {integrity: sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
- keyv@4.5.4:
- resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
-
- levn@0.4.1:
- resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
- engines: {node: '>= 0.8.0'}
-
- lightningcss-android-arm64@1.30.2:
- resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [android]
-
- lightningcss-darwin-arm64@1.30.2:
- resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [darwin]
-
- lightningcss-darwin-x64@1.30.2:
- resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [darwin]
-
- lightningcss-freebsd-x64@1.30.2:
- resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [freebsd]
-
- lightningcss-linux-arm-gnueabihf@1.30.2:
- resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm]
- os: [linux]
-
- lightningcss-linux-arm64-gnu@1.30.2:
- resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
- 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==}
- engines: {node: '>= 12.0.0'}
- cpu: [arm64]
- os: [win32]
-
- lightningcss-win32-x64-msvc@1.30.2:
- resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
- engines: {node: '>= 12.0.0'}
- cpu: [x64]
- os: [win32]
-
- lightningcss@1.30.2:
- resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
- engines: {node: '>= 12.0.0'}
-
- lines-and-columns@1.2.4:
- resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
-
- lint-staged@16.2.7:
- resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==}
- engines: {node: '>=20.17'}
- hasBin: true
-
- listr2@9.0.5:
- resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
- engines: {node: '>=20.0.0'}
-
- local-pkg@1.1.2:
- resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
- engines: {node: '>=14'}
-
- locate-path@5.0.0:
- resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
- engines: {node: '>=8'}
-
- locate-path@6.0.0:
- resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
- engines: {node: '>=10'}
-
- lodash-es@4.17.22:
- resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==}
-
- lodash.merge@4.6.2:
- resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
-
- lodash@4.17.21:
- resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
-
- log-update@6.1.0:
- resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
- engines: {node: '>=18'}
-
- longest-streak@3.1.0:
- resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
-
- loose-envify@1.4.0:
- resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
- hasBin: true
-
- lower-case@2.0.2:
- resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
-
- lru-cache@5.1.1:
- resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
-
- lucide-react@0.562.0:
- resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==}
- peerDependencies:
- react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
- magic-string@0.30.21:
- resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
-
- markdown-table@3.0.4:
- resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
-
- math-intrinsics@1.1.0:
- resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
- engines: {node: '>= 0.4'}
-
- mdast-util-find-and-replace@3.0.2:
- resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
-
- mdast-util-from-markdown@2.0.2:
- resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==}
-
- mdast-util-frontmatter@2.0.1:
- resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==}
-
- mdast-util-gfm-autolink-literal@2.0.1:
- resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
-
- mdast-util-gfm-footnote@2.1.0:
- resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
-
- mdast-util-gfm-strikethrough@2.0.0:
- resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
-
- mdast-util-gfm-table@2.0.0:
- resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
-
- mdast-util-gfm-task-list-item@2.0.0:
- resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
-
- mdast-util-gfm@3.1.0:
- resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
-
- mdast-util-mdx-expression@2.0.1:
- resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
-
- mdast-util-mdx-jsx@3.2.0:
- resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
-
- mdast-util-mdxjs-esm@2.0.1:
- resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
-
- mdast-util-phrasing@4.1.0:
- resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
-
- mdast-util-to-hast@13.2.1:
- resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
-
- mdast-util-to-markdown@2.1.2:
- resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
-
- mdast-util-to-string@4.0.0:
- resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
-
- micromark-core-commonmark@2.0.3:
- resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
-
- micromark-extension-frontmatter@2.0.0:
- resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==}
-
- micromark-extension-gfm-autolink-literal@2.1.0:
- resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
-
- micromark-extension-gfm-footnote@2.1.0:
- resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
-
- micromark-extension-gfm-strikethrough@2.1.0:
- resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
-
- micromark-extension-gfm-table@2.1.1:
- resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
-
- micromark-extension-gfm-tagfilter@2.0.0:
- resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
-
- micromark-extension-gfm-task-list-item@2.1.0:
- resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
-
- micromark-extension-gfm@3.0.0:
- resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
-
- micromark-factory-destination@2.0.1:
- resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
-
- micromark-factory-label@2.0.1:
- resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
-
- micromark-factory-space@2.0.1:
- resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
-
- micromark-factory-title@2.0.1:
- resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
-
- micromark-factory-whitespace@2.0.1:
- resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
-
- micromark-util-character@2.1.1:
- resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
-
- micromark-util-chunked@2.0.1:
- resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
-
- micromark-util-classify-character@2.0.1:
- resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
-
- micromark-util-combine-extensions@2.0.1:
- resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
-
- micromark-util-decode-numeric-character-reference@2.0.2:
- resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
-
- micromark-util-decode-string@2.0.1:
- resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
-
- micromark-util-encode@2.0.1:
- resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
-
- micromark-util-html-tag-name@2.0.1:
- resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
-
- micromark-util-normalize-identifier@2.0.1:
- resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
-
- micromark-util-resolve-all@2.0.1:
- resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
-
- micromark-util-sanitize-uri@2.0.1:
- resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
-
- micromark-util-subtokenize@2.1.0:
- resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
-
- micromark-util-symbol@2.0.1:
- resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
-
- micromark-util-types@2.0.2:
- resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
-
- micromark@4.0.2:
- resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
-
- micromatch@4.0.8:
- resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
- engines: {node: '>=8.6'}
-
- mime-db@1.52.0:
- resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
- engines: {node: '>= 0.6'}
-
- mime-types@2.1.35:
- resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
- engines: {node: '>= 0.6'}
-
- mimic-function@5.0.1:
- resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
- engines: {node: '>=18'}
-
- minimatch@3.1.2:
- resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
-
- minimatch@9.0.5:
- resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
- engines: {node: '>=16 || 14 >=14.17'}
-
- mlly@1.8.0:
- resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
-
- ms@2.1.3:
- resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
-
- nano-spawn@2.0.0:
- resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==}
- engines: {node: '>=20.17'}
-
- nanoid@3.3.11:
- resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
- engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
- hasBin: true
-
- natural-compare@1.4.0:
- resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
-
- natural-orderby@5.0.0:
- resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==}
- engines: {node: '>=18'}
-
- next-themes@0.4.6:
- resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
- peerDependencies:
- react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
-
- no-case@3.0.4:
- resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
-
- node-releases@2.0.27:
- resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
-
- normalize-path@3.0.0:
- resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
- engines: {node: '>=0.10.0'}
-
- nth-check@2.1.1:
- resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
-
- object-assign@4.1.1:
- resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
- engines: {node: '>=0.10.0'}
-
- object-deep-merge@2.0.0:
- resolution: {integrity: sha512-3DC3UMpeffLTHiuXSy/UG4NOIYTLlY9u3V82+djSCLYClWobZiS4ivYzpIUWrRY/nfsJ8cWsKyG3QfyLePmhvg==}
-
- onetime@7.0.0:
- resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
- engines: {node: '>=18'}
-
- optionator@0.9.4:
- resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
- engines: {node: '>= 0.8.0'}
-
- p-limit@2.3.0:
- resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
- engines: {node: '>=6'}
-
- p-limit@3.1.0:
- resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
- engines: {node: '>=10'}
-
- p-locate@4.1.0:
- resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
- engines: {node: '>=8'}
-
- p-locate@5.0.0:
- resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
- engines: {node: '>=10'}
-
- p-try@2.2.0:
- resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
- engines: {node: '>=6'}
-
- package-manager-detector@1.6.0:
- resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
-
- parent-module@1.0.1:
- resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
- engines: {node: '>=6'}
-
- parse-entities@4.0.2:
- resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
-
- parse-gitignore@2.0.0:
- resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==}
- engines: {node: '>=14'}
-
- parse-imports-exports@0.2.4:
- resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
-
- parse-json@5.2.0:
- resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
- engines: {node: '>=8'}
-
- parse-numeric-range@1.3.0:
- resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
-
- parse-statements@1.0.11:
- resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==}
-
- parse5@7.3.0:
- resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
-
- path-exists@4.0.0:
- resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
- engines: {node: '>=8'}
-
- path-key@3.1.1:
- resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
- engines: {node: '>=8'}
-
- path-type@4.0.0:
- resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
- engines: {node: '>=8'}
-
- pathe@2.0.3:
- resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
-
- picocolors@1.1.1:
- resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
-
- picomatch@2.3.1:
- resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
- engines: {node: '>=8.6'}
-
- picomatch@4.0.3:
- resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
- engines: {node: '>=12'}
-
- pidtree@0.6.0:
- resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
- engines: {node: '>=0.10'}
- hasBin: true
-
- pkg-types@1.3.1:
- resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
-
- pkg-types@2.3.0:
- resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
-
- pluralize@8.0.0:
- resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
- engines: {node: '>=4'}
-
- pngjs@5.0.0:
- resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
- engines: {node: '>=10.13.0'}
-
- pnpm-workspace-yaml@1.5.0:
- resolution: {integrity: sha512-PxdyJuFvq5B0qm3s9PaH/xOtSxrcvpBRr+BblhucpWjs8c79d4b7/cXhyY4AyHOHCnqklCYZTjfl0bT/mFVTRw==}
-
- postcss-selector-parser@6.0.10:
- resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
- engines: {node: '>=4'}
-
- postcss-selector-parser@7.1.1:
- resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
- engines: {node: '>=4'}
-
- postcss@8.5.6:
- resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
- engines: {node: ^10 || ^12 || >=14}
-
- prelude-ls@1.2.1:
- resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
- engines: {node: '>= 0.8.0'}
-
- prettier@3.8.0:
- resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
- engines: {node: '>=14'}
- hasBin: true
-
- prop-types@15.8.1:
- resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
-
- property-information@6.5.0:
- resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
-
- property-information@7.1.0:
- resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
-
- proxy-from-env@1.1.0:
- resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
-
- punycode@2.3.1:
- resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
- engines: {node: '>=6'}
-
- qrcode@1.5.4:
- resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
- engines: {node: '>=10.13.0'}
- hasBin: true
-
- quansync@0.2.11:
- resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
-
- react-dom@19.2.3:
- resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
- peerDependencies:
- react: ^19.2.3
-
- react-hook-form@7.71.1:
- resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==}
- engines: {node: '>=18.0.0'}
- peerDependencies:
- react: ^16.8.0 || ^17 || ^18 || ^19
-
- react-is@16.13.1:
- resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
-
- react-is@18.3.1:
- resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
-
- react-markdown@10.1.0:
- resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
- peerDependencies:
- '@types/react': '>=18'
- react: '>=18'
-
- react-markdown@9.0.3:
- resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
- peerDependencies:
- '@types/react': '>=18'
- react: '>=18'
-
- react-refresh@0.18.0:
- resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
- engines: {node: '>=0.10.0'}
-
- react-remove-scroll-bar@2.3.8:
- resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
- engines: {node: '>=10'}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- react-remove-scroll@2.7.2:
- resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
- engines: {node: '>=10'}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- react-smooth@4.0.4:
- resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
- react-style-singleton@2.2.3:
- resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
- engines: {node: '>=10'}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- react-transition-group@4.4.5:
- resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
- peerDependencies:
- react: '>=16.6.0'
- react-dom: '>=16.6.0'
-
- react@19.2.3:
- resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
- engines: {node: '>=0.10.0'}
-
- readdirp@3.6.0:
- resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
- engines: {node: '>=8.10.0'}
-
- recast@0.23.11:
- resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
- engines: {node: '>= 4'}
-
- recharts-scale@0.4.5:
- resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
-
- recharts@2.15.4:
- resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==}
- engines: {node: '>=14'}
- peerDependencies:
- react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
- refa@0.12.1:
- resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
- engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
- refractor@4.9.0:
- resolution: {integrity: sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==}
-
- regexp-ast-analysis@0.7.1:
- resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==}
- engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
- regexp-tree@0.1.27:
- resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
- hasBin: true
-
- regjsparser@0.13.0:
- resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
- hasBin: true
-
- rehype-attr@3.0.3:
- resolution: {integrity: sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==}
- engines: {node: '>=16'}
-
- rehype-autolink-headings@7.1.0:
- resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
-
- rehype-ignore@2.0.3:
- resolution: {integrity: sha512-IzhP6/u/6sm49sdktuYSmeIuObWB+5yC/5eqVws8BhuGA9kY25/byz6uCy/Ravj6lXUShEd2ofHM5MyAIj86Sg==}
- engines: {node: '>=16'}
-
- rehype-parse@9.0.1:
- resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
-
- rehype-prism-plus@2.0.0:
- resolution: {integrity: sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==}
-
- rehype-prism-plus@2.0.1:
- resolution: {integrity: sha512-Wglct0OW12tksTUseAPyWPo3srjBOY7xKlql/DPKi7HbsdZTyaLCAoO58QBKSczFQxElTsQlOY3JDOFzB/K++Q==}
-
- rehype-raw@7.0.0:
- resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
-
- rehype-rewrite@4.0.4:
- resolution: {integrity: sha512-L/FO96EOzSA6bzOam4DVu61/PB3AGKcSPXpa53yMIozoxH4qg1+bVZDF8zh1EsuxtSauAhzt5cCnvoplAaSLrw==}
- engines: {node: '>=16.0.0'}
-
- rehype-slug@6.0.0:
- resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
-
- rehype-stringify@10.0.1:
- resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
-
- rehype@13.0.2:
- resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==}
-
- remark-gfm@4.0.1:
- resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
-
- remark-github-blockquote-alert@1.3.1:
- resolution: {integrity: sha512-OPNnimcKeozWN1w8KVQEuHOxgN3L4rah8geMOLhA5vN9wITqU4FWD+G26tkEsCGHiOVDbISx+Se5rGZ+D1p0Jg==}
- engines: {node: '>=16'}
-
- remark-parse@11.0.0:
- resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
-
- remark-rehype@11.1.2:
- resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
-
- remark-stringify@11.0.0:
- resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
-
- require-directory@2.1.1:
- resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
- engines: {node: '>=0.10.0'}
-
- require-main-filename@2.0.0:
- resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
-
- reserved-identifiers@1.2.0:
- resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
- engines: {node: '>=18'}
-
- resolve-from@4.0.0:
- resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
- engines: {node: '>=4'}
-
- resolve-pkg-maps@1.0.0:
- resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
-
- restore-cursor@5.1.0:
- resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
- engines: {node: '>=18'}
-
- rfdc@1.4.1:
- resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
-
- rollup@4.55.2:
- resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==}
- engines: {node: '>=18.0.0', npm: '>=8.0.0'}
- hasBin: true
-
- scheduler@0.27.0:
- resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
-
- scslre@0.3.0:
- resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
- engines: {node: ^14.0.0 || >=16.0.0}
-
- semver@6.3.1:
- resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
- hasBin: true
-
- semver@7.7.3:
- resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
- engines: {node: '>=10'}
- hasBin: true
-
- seroval-plugins@1.4.2:
- resolution: {integrity: sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA==}
- engines: {node: '>=10'}
- peerDependencies:
- seroval: ^1.0
-
- seroval@1.4.2:
- resolution: {integrity: sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==}
- engines: {node: '>=10'}
-
- set-blocking@2.0.0:
- resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
-
- shebang-command@2.0.0:
- resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
- engines: {node: '>=8'}
-
- shebang-regex@3.0.0:
- resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
- engines: {node: '>=8'}
-
- signal-exit@4.1.0:
- resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
- engines: {node: '>=14'}
-
- simple-git-hooks@2.13.1:
- resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==}
- hasBin: true
-
- sisteransi@1.0.5:
- resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
-
- slice-ansi@7.1.2:
- resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==}
- engines: {node: '>=18'}
-
- snake-case@3.0.4:
- resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
-
- sonner@2.0.7:
- resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
- peerDependencies:
- react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
-
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
- engines: {node: '>=0.10.0'}
-
- source-map@0.6.1:
- resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
- engines: {node: '>=0.10.0'}
-
- source-map@0.7.6:
- resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
- engines: {node: '>= 12'}
-
- space-separated-tokens@2.0.2:
- resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
-
- spdx-exceptions@2.5.0:
- resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
-
- spdx-expression-parse@4.0.0:
- resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==}
-
- spdx-license-ids@3.0.22:
- resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==}
-
- string-argv@0.3.2:
- resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
- engines: {node: '>=0.6.19'}
-
- string-ts@2.3.1:
- resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==}
-
- string-width@4.2.3:
- resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
- engines: {node: '>=8'}
-
- string-width@7.2.0:
- resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
- engines: {node: '>=18'}
-
- string-width@8.1.0:
- resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==}
- engines: {node: '>=20'}
-
- stringify-entities@4.0.4:
- resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
-
- strip-ansi@6.0.1:
- resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
- engines: {node: '>=8'}
-
- strip-ansi@7.1.2:
- resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
- engines: {node: '>=12'}
-
- strip-indent@4.1.1:
- resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==}
- engines: {node: '>=12'}
-
- strip-json-comments@3.1.1:
- resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
- engines: {node: '>=8'}
-
- style-to-js@1.1.21:
- resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
-
- style-to-object@1.0.14:
- resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
-
- supports-color@7.2.0:
- resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
- engines: {node: '>=8'}
-
- svg-parser@2.0.4:
- resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
-
- synckit@0.11.12:
- resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
- engines: {node: ^14.18.0 || >=16.0.0}
-
- tagged-tag@1.0.0:
- resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
- engines: {node: '>=20'}
-
- tailwind-merge@3.4.0:
- resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
-
- tailwindcss@4.1.18:
- resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
-
- tapable@2.3.0:
- resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
- engines: {node: '>=6'}
-
- tiny-invariant@1.3.3:
- resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
-
- tiny-warning@1.0.3:
- resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
-
- tinyexec@1.0.2:
- resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
- engines: {node: '>=18'}
-
- tinyglobby@0.2.15:
- resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
- engines: {node: '>=12.0.0'}
-
- to-regex-range@5.0.1:
- resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
- engines: {node: '>=8.0'}
-
- to-valid-identifier@1.0.0:
- resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==}
- engines: {node: '>=20'}
-
- toml-eslint-parser@0.10.1:
- resolution: {integrity: sha512-9mjy3frhioGIVGcwamlVlUyJ9x+WHw/TXiz9R4YOlmsIuBN43r9Dp8HZ35SF9EKjHrn3BUZj04CF+YqZ2oJ+7w==}
- engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-
- trim-lines@3.0.1:
- resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
-
- trough@2.2.0:
- resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
-
- ts-api-utils@2.4.0:
- resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
- engines: {node: '>=18.12'}
- peerDependencies:
- typescript: '>=4.8.4'
-
- ts-declaration-location@1.0.7:
- resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==}
- peerDependencies:
- typescript: '>=4.0.0'
-
- ts-pattern@5.9.0:
- resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==}
-
- tslib@2.8.1:
- resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
-
- tsx@4.21.0:
- resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
- engines: {node: '>=18.0.0'}
- hasBin: true
-
- tw-animate-css@1.4.0:
- resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
-
- type-check@0.4.0:
- resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
- engines: {node: '>= 0.8.0'}
-
- type-fest@5.4.1:
- resolution: {integrity: sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ==}
- engines: {node: '>=20'}
-
- typescript-eslint@8.53.1:
- resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
- typescript: '>=4.8.4 <6.0.0'
-
- typescript@5.9.3:
- resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
- engines: {node: '>=14.17'}
- hasBin: true
-
- ufo@1.6.3:
- resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
-
- undici-types@7.16.0:
- resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
-
- unified@11.0.5:
- resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
-
- unist-util-filter@5.0.1:
- resolution: {integrity: sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==}
-
- unist-util-is@6.0.1:
- resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
-
- unist-util-position@5.0.0:
- resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
-
- unist-util-stringify-position@4.0.0:
- resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
-
- unist-util-visit-parents@6.0.2:
- resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
-
- unist-util-visit@5.0.0:
- resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
-
- unplugin@2.3.11:
- resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
- engines: {node: '>=18.12.0'}
-
- update-browserslist-db@1.2.3:
- resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
-
- uri-js@4.4.1:
- resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
-
- use-callback-ref@1.3.3:
- resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
- engines: {node: '>=10'}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- use-sidecar@1.1.3:
- resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
- engines: {node: '>=10'}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- use-sync-external-store@1.6.0:
- resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
- utf8@3.0.0:
- resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==}
-
- util-deprecate@1.0.2:
- resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
-
- vaul@1.1.2:
- resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
- peerDependencies:
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
- react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
-
- vfile-location@5.0.3:
- resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
-
- vfile-message@4.0.3:
- resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
-
- vfile@6.0.3:
- resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
-
- victory-vendor@36.9.2:
- resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
-
- vite-plugin-svgr@4.5.0:
- resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==}
- peerDependencies:
- vite: '>=2.6.0'
-
- vite@7.3.1:
- resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
- peerDependencies:
- '@types/node': ^20.19.0 || >=22.12.0
- jiti: '>=1.21.0'
- less: ^4.0.0
- lightningcss: ^1.21.0
- sass: ^1.70.0
- sass-embedded: ^1.70.0
- stylus: '>=0.54.8'
- sugarss: ^5.0.0
- terser: ^5.16.0
- tsx: ^4.8.1
- yaml: ^2.4.2
- peerDependenciesMeta:
- '@types/node':
- optional: true
- jiti:
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- sass-embedded:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
- tsx:
- optional: true
- yaml:
- optional: true
-
- vue-eslint-parser@10.2.0:
- resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- peerDependencies:
- eslint: ^8.57.0 || ^9.0.0
-
- web-namespaces@2.0.1:
- resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
-
- webpack-virtual-modules@0.6.2:
- resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
-
- which-module@2.0.1:
- resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
-
- which@2.0.2:
- resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
- engines: {node: '>= 8'}
- hasBin: true
-
- word-wrap@1.2.5:
- resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
- engines: {node: '>=0.10.0'}
-
- wrap-ansi@6.2.0:
- resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
- engines: {node: '>=8'}
-
- wrap-ansi@9.0.2:
- resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
- engines: {node: '>=18'}
-
- xml-name-validator@4.0.0:
- resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
- engines: {node: '>=12'}
-
- y18n@4.0.3:
- resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
-
- yallist@3.1.1:
- resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
-
- yaml-eslint-parser@1.3.2:
- resolution: {integrity: sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==}
- engines: {node: ^14.17.0 || >=16.0.0}
-
- yaml-eslint-parser@2.0.0:
- resolution: {integrity: sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-
- yaml@2.8.2:
- resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
- engines: {node: '>= 14.6'}
- hasBin: true
-
- yargs-parser@18.1.3:
- resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
- engines: {node: '>=6'}
-
- yargs@15.4.1:
- resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
- engines: {node: '>=8'}
-
- yocto-queue@0.1.0:
- resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
- engines: {node: '>=10'}
-
- zod-validation-error@4.0.2:
- resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
- engines: {node: '>=18.0.0'}
- peerDependencies:
- zod: ^3.25.0 || ^4.0.0
-
- zod@3.25.76:
- resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
-
- zod@4.3.5:
- resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
-
- zustand@5.0.10:
- resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==}
- engines: {node: '>=12.20.0'}
- peerDependencies:
- '@types/react': '>=18.0.0'
- immer: '>=9.0.6'
- react: '>=18.0.0'
- use-sync-external-store: '>=1.2.0'
- peerDependenciesMeta:
- '@types/react':
- optional: true
- immer:
- optional: true
- react:
- optional: true
- use-sync-external-store:
- optional: true
-
- zwitch@2.0.4:
- resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
-
-snapshots:
-
- '@antfu/eslint-config@6.7.3(@eslint-react/eslint-plugin@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.27)(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@antfu/install-pkg': 1.1.0
- '@clack/prompts': 0.11.0
- '@eslint-community/eslint-plugin-eslint-comments': 4.6.0(eslint@9.39.2(jiti@2.6.1))
- '@eslint/markdown': 7.5.1
- '@stylistic/eslint-plugin': 5.7.0(eslint@9.39.2(jiti@2.6.1))
- '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@vitest/eslint-plugin': 1.6.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- ansis: 4.2.0
- cac: 6.7.14
- eslint: 9.39.2(jiti@2.6.1)
- eslint-config-flat-gitignore: 2.1.0(eslint@9.39.2(jiti@2.6.1))
- eslint-flat-config-utils: 2.1.4
- eslint-merge-processors: 2.0.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-antfu: 3.1.3(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-command: 3.4.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-import-lite: 0.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-jsdoc: 61.7.1(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-jsonc: 2.21.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-n: 17.23.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-no-only-tests: 3.3.0
- eslint-plugin-perfectionist: 4.15.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-pnpm: 1.5.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-toml: 0.12.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-unicorn: 62.0.0(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-vue: 10.7.0(@stylistic/eslint-plugin@5.7.0(eslint@9.39.2(jiti@2.6.1)))(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1)))
- eslint-plugin-yml: 1.19.1(eslint@9.39.2(jiti@2.6.1))
- eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.27)(eslint@9.39.2(jiti@2.6.1))
- globals: 16.5.0
- jsonc-eslint-parser: 2.4.2
- local-pkg: 1.1.2
- parse-gitignore: 2.0.0
- toml-eslint-parser: 0.10.1
- vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@2.6.1))
- yaml-eslint-parser: 1.3.2
- optionalDependencies:
- '@eslint-react/eslint-plugin': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-react-refresh: 0.4.26(eslint@9.39.2(jiti@2.6.1))
- transitivePeerDependencies:
- - '@eslint/json'
- - '@vue/compiler-sfc'
- - supports-color
- - typescript
- - vitest
-
- '@antfu/install-pkg@1.1.0':
- dependencies:
- package-manager-detector: 1.6.0
- tinyexec: 1.0.2
-
- '@babel/code-frame@7.28.6':
- dependencies:
- '@babel/helper-validator-identifier': 7.28.5
- js-tokens: 4.0.0
- picocolors: 1.1.1
-
- '@babel/compat-data@7.28.6': {}
-
- '@babel/core@7.28.6':
- dependencies:
- '@babel/code-frame': 7.28.6
- '@babel/generator': 7.28.6
- '@babel/helper-compilation-targets': 7.28.6
- '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
- '@babel/helpers': 7.28.6
- '@babel/parser': 7.28.6
- '@babel/template': 7.28.6
- '@babel/traverse': 7.28.6
- '@babel/types': 7.28.6
- '@jridgewell/remapping': 2.3.5
- convert-source-map: 2.0.0
- debug: 4.4.3
- gensync: 1.0.0-beta.2
- json5: 2.2.3
- semver: 6.3.1
- transitivePeerDependencies:
- - supports-color
-
- '@babel/generator@7.28.6':
- dependencies:
- '@babel/parser': 7.28.6
- '@babel/types': 7.28.6
- '@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.31
- jsesc: 3.1.0
-
- '@babel/helper-compilation-targets@7.28.6':
- dependencies:
- '@babel/compat-data': 7.28.6
- '@babel/helper-validator-option': 7.27.1
- browserslist: 4.28.1
- lru-cache: 5.1.1
- semver: 6.3.1
-
- '@babel/helper-globals@7.28.0': {}
-
- '@babel/helper-module-imports@7.28.6':
- dependencies:
- '@babel/traverse': 7.28.6
- '@babel/types': 7.28.6
- transitivePeerDependencies:
- - supports-color
-
- '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/helper-module-imports': 7.28.6
- '@babel/helper-validator-identifier': 7.28.5
- '@babel/traverse': 7.28.6
- transitivePeerDependencies:
- - supports-color
-
- '@babel/helper-plugin-utils@7.28.6': {}
-
- '@babel/helper-string-parser@7.27.1': {}
-
- '@babel/helper-validator-identifier@7.28.5': {}
-
- '@babel/helper-validator-option@7.27.1': {}
-
- '@babel/helpers@7.28.6':
- dependencies:
- '@babel/template': 7.28.6
- '@babel/types': 7.28.6
-
- '@babel/parser@7.28.6':
- dependencies:
- '@babel/types': 7.28.6
-
- '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/runtime@7.28.6': {}
-
- '@babel/template@7.28.6':
- dependencies:
- '@babel/code-frame': 7.28.6
- '@babel/parser': 7.28.6
- '@babel/types': 7.28.6
-
- '@babel/traverse@7.28.6':
- dependencies:
- '@babel/code-frame': 7.28.6
- '@babel/generator': 7.28.6
- '@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.6
- '@babel/template': 7.28.6
- '@babel/types': 7.28.6
- debug: 4.4.3
- transitivePeerDependencies:
- - supports-color
-
- '@babel/types@7.28.6':
- dependencies:
- '@babel/helper-string-parser': 7.27.1
- '@babel/helper-validator-identifier': 7.28.5
-
- '@clack/core@0.5.0':
- dependencies:
- picocolors: 1.1.1
- sisteransi: 1.0.5
-
- '@clack/prompts@0.11.0':
- dependencies:
- '@clack/core': 0.5.0
- picocolors: 1.1.1
- sisteransi: 1.0.5
-
- '@dnd-kit/accessibility@3.1.1(react@19.2.3)':
- dependencies:
- react: 19.2.3
- tslib: 2.8.1
-
- '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@dnd-kit/accessibility': 3.1.1(react@19.2.3)
- '@dnd-kit/utilities': 3.2.2(react@19.2.3)
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- tslib: 2.8.1
-
- '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@dnd-kit/utilities': 3.2.2(react@19.2.3)
- react: 19.2.3
- tslib: 2.8.1
-
- '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@dnd-kit/utilities': 3.2.2(react@19.2.3)
- react: 19.2.3
- tslib: 2.8.1
-
- '@dnd-kit/utilities@3.2.2(react@19.2.3)':
- dependencies:
- react: 19.2.3
- tslib: 2.8.1
-
- '@es-joy/jsdoccomment@0.78.0':
- dependencies:
- '@types/estree': 1.0.8
- '@typescript-eslint/types': 8.53.1
- comment-parser: 1.4.1
- esquery: 1.7.0
- jsdoc-type-pratt-parser: 7.0.0
-
- '@es-joy/resolve.exports@1.2.0': {}
-
- '@esbuild/aix-ppc64@0.27.2':
- optional: true
-
- '@esbuild/android-arm64@0.27.2':
- optional: true
-
- '@esbuild/android-arm@0.27.2':
- optional: true
-
- '@esbuild/android-x64@0.27.2':
- optional: true
-
- '@esbuild/darwin-arm64@0.27.2':
- optional: true
-
- '@esbuild/darwin-x64@0.27.2':
- optional: true
-
- '@esbuild/freebsd-arm64@0.27.2':
- optional: true
-
- '@esbuild/freebsd-x64@0.27.2':
- optional: true
-
- '@esbuild/linux-arm64@0.27.2':
- optional: true
-
- '@esbuild/linux-arm@0.27.2':
- optional: true
-
- '@esbuild/linux-ia32@0.27.2':
- optional: true
-
- '@esbuild/linux-loong64@0.27.2':
- optional: true
-
- '@esbuild/linux-mips64el@0.27.2':
- optional: true
-
- '@esbuild/linux-ppc64@0.27.2':
- optional: true
-
- '@esbuild/linux-riscv64@0.27.2':
- optional: true
-
- '@esbuild/linux-s390x@0.27.2':
- optional: true
-
- '@esbuild/linux-x64@0.27.2':
- optional: true
-
- '@esbuild/netbsd-arm64@0.27.2':
- optional: true
-
- '@esbuild/netbsd-x64@0.27.2':
- optional: true
-
- '@esbuild/openbsd-arm64@0.27.2':
- optional: true
-
- '@esbuild/openbsd-x64@0.27.2':
- optional: true
-
- '@esbuild/openharmony-arm64@0.27.2':
- optional: true
-
- '@esbuild/sunos-x64@0.27.2':
- optional: true
-
- '@esbuild/win32-arm64@0.27.2':
- optional: true
-
- '@esbuild/win32-ia32@0.27.2':
- optional: true
-
- '@esbuild/win32-x64@0.27.2':
- optional: true
-
- '@eslint-community/eslint-plugin-eslint-comments@4.6.0(eslint@9.39.2(jiti@2.6.1))':
- dependencies:
- escape-string-regexp: 4.0.0
- eslint: 9.39.2(jiti@2.6.1)
- ignore: 7.0.5
-
- '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
- eslint-visitor-keys: 3.4.3
-
- '@eslint-community/regexpp@4.12.2': {}
-
- '@eslint-react/ast@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-react/eff': 2.7.2
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- string-ts: 2.3.1
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@eslint-react/core@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/var': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- birecord: 0.1.1
- eslint: 9.39.2(jiti@2.6.1)
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@eslint-react/eff@2.7.2': {}
-
- '@eslint-react/eslint-plugin@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- eslint-plugin-react-dom: 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-react-hooks-extra: 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-react-naming-convention: 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-react-web-api: 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-react-x: 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- ts-api-utils: 2.4.0(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@eslint-react/shared@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-react/eff': 2.7.2
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- ts-pattern: 5.9.0
- typescript: 5.9.3
- zod: 4.3.5
- transitivePeerDependencies:
- - supports-color
-
- '@eslint-react/var@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))':
- dependencies:
- '@eslint/core': 0.17.0
- optionalDependencies:
- eslint: 9.39.2(jiti@2.6.1)
-
- '@eslint/config-array@0.21.1':
- dependencies:
- '@eslint/object-schema': 2.1.7
- debug: 4.4.3
- minimatch: 3.1.2
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/config-helpers@0.4.2':
- dependencies:
- '@eslint/core': 0.17.0
-
- '@eslint/core@0.17.0':
- dependencies:
- '@types/json-schema': 7.0.15
-
- '@eslint/eslintrc@3.3.3':
- dependencies:
- ajv: 6.12.6
- debug: 4.4.3
- espree: 10.4.0
- globals: 14.0.0
- ignore: 5.3.2
- import-fresh: 3.3.1
- js-yaml: 4.1.1
- minimatch: 3.1.2
- strip-json-comments: 3.1.1
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/js@9.39.2': {}
-
- '@eslint/markdown@7.5.1':
- dependencies:
- '@eslint/core': 0.17.0
- '@eslint/plugin-kit': 0.4.1
- github-slugger: 2.0.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-frontmatter: 2.0.1
- mdast-util-gfm: 3.1.0
- micromark-extension-frontmatter: 2.0.0
- micromark-extension-gfm: 3.0.0
- micromark-util-normalize-identifier: 2.0.1
- transitivePeerDependencies:
- - supports-color
-
- '@eslint/object-schema@2.1.7': {}
-
- '@eslint/plugin-kit@0.4.1':
- dependencies:
- '@eslint/core': 0.17.0
- levn: 0.4.1
-
- '@floating-ui/core@1.7.3':
- dependencies:
- '@floating-ui/utils': 0.2.10
-
- '@floating-ui/dom@1.7.4':
- dependencies:
- '@floating-ui/core': 1.7.3
- '@floating-ui/utils': 0.2.10
-
- '@floating-ui/react-dom@2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@floating-ui/dom': 1.7.4
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
-
- '@floating-ui/utils@0.2.10': {}
-
- '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.3))':
- dependencies:
- '@standard-schema/utils': 0.3.0
- react-hook-form: 7.71.1(react@19.2.3)
-
- '@humanfs/core@0.19.1': {}
-
- '@humanfs/node@0.16.7':
- dependencies:
- '@humanfs/core': 0.19.1
- '@humanwhocodes/retry': 0.4.3
-
- '@humanwhocodes/module-importer@1.0.1': {}
-
- '@humanwhocodes/retry@0.4.3': {}
-
- '@jridgewell/gen-mapping@0.3.13':
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
- '@jridgewell/trace-mapping': 0.3.31
-
- '@jridgewell/remapping@2.3.5':
- dependencies:
- '@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.31
-
- '@jridgewell/resolve-uri@3.1.2': {}
-
- '@jridgewell/sourcemap-codec@1.5.5': {}
-
- '@jridgewell/trace-mapping@0.3.31':
- dependencies:
- '@jridgewell/resolve-uri': 3.1.2
- '@jridgewell/sourcemap-codec': 1.5.5
-
- '@marsidev/react-turnstile@1.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
-
- '@pkgr/core@0.2.9': {}
-
- '@radix-ui/number@1.1.1': {}
-
- '@radix-ui/primitive@1.1.3': {}
-
- '@radix-ui/react-arrow@1.1.7(@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/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)
- 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-avatar@1.1.11(@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/react-context': 1.1.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-primitive': 2.1.4(@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-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-layout-effect': 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-checkbox@1.3.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)':
- 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-presence': 1.1.5(@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-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-collection@1.1.7(@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/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-slot': 1.2.3(@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-compose-refs@1.1.2(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-context@1.1.2(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-context@1.1.3(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-dialog@1.1.15(@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-dismissable-layer': 1.1.11(@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-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-focus-scope': 1.1.7(@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-id': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-portal': 1.1.9(@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-presence': 1.1.5(@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-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-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3)
- aria-hidden: 1.2.6
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3)
- optionalDependencies:
- '@types/react': 19.2.8
- '@types/react-dom': 19.2.3(@types/react@19.2.8)
-
- '@radix-ui/react-direction@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-dismissable-layer@1.1.11(@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-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-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-escape-keydown': 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-dropdown-menu@2.1.16(@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-id': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-menu': 2.1.16(@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-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)
- 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-focus-guards@1.1.3(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-focus-scope@1.1.7(@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/react-compose-refs': 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-callback-ref': 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-id@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-label@2.1.8(@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/react-primitive': 2.1.4(@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)
- 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-menu@2.1.16(@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-collection': 1.1.7(@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-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-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-dismissable-layer': 1.1.11(@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-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-focus-scope': 1.1.7(@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-id': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-popper': 1.2.8(@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-portal': 1.1.9(@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-presence': 1.1.5(@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-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-roving-focus': 1.1.11(@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-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- aria-hidden: 1.2.6
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3)
- optionalDependencies:
- '@types/react': 19.2.8
- '@types/react-dom': 19.2.3(@types/react@19.2.8)
-
- '@radix-ui/react-popper@1.2.8(@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:
- '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@radix-ui/react-arrow': 1.1.7(@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-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-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-rect': 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)
- '@radix-ui/rect': 1.1.1
- 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-portal@1.1.9(@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/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-layout-effect': 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-presence@1.1.5(@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/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-layout-effect': 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-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)':
- dependencies:
- '@radix-ui/react-slot': 1.2.3(@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-primitive@2.1.4(@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/react-slot': 1.2.4(@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-roving-focus@1.1.11(@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-collection': 1.1.7(@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-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-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-id': 1.1.1(@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-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-controllable-state': 1.2.2(@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-select@2.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/number': 1.1.1
- '@radix-ui/primitive': 1.1.3
- '@radix-ui/react-collection': 1.1.7(@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-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-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-dismissable-layer': 1.1.11(@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-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-focus-scope': 1.1.7(@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-id': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-popper': 1.2.8(@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-portal': 1.1.9(@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-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-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(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-layout-effect': 1.1.1(@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-visually-hidden': 1.2.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)
- aria-hidden: 1.2.6
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-remove-scroll: 2.7.2(@types/react@19.2.8)(react@19.2.3)
- optionalDependencies:
- '@types/react': 19.2.8
- '@types/react-dom': 19.2.3(@types/react@19.2.8)
-
- '@radix-ui/react-separator@1.1.8(@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/react-primitive': 2.1.4(@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)
- 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-slot@1.2.3(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-slot@1.2.4(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@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
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-presence': 1.1.5(@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-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-roving-focus': 1.1.11(@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)
- 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-toggle-group@1.1.11(@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-context': 1.1.2(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-direction': 1.1.1(@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-roving-focus': 1.1.11(@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-toggle': 1.1.10(@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)
- 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-toggle@1.1.10(@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-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)
- 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-tooltip@1.2.8(@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-dismissable-layer': 1.1.11(@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-id': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-popper': 1.2.8(@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-portal': 1.1.9(@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-presence': 1.1.5(@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-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-slot': 1.2.3(@types/react@19.2.8)(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-visually-hidden': 1.2.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)
- 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-use-callback-ref@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.3)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- use-sync-external-store: 1.6.0(react@19.2.3)
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/rect': 1.1.1
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-use-size@1.1.1(@types/react@19.2.8)(react@19.2.3)':
- dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3)
- react: 19.2.3
- optionalDependencies:
- '@types/react': 19.2.8
-
- '@radix-ui/react-visually-hidden@1.2.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)':
- dependencies:
- '@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)
- 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/rect@1.1.1': {}
-
- '@rolldown/pluginutils@1.0.0-beta.53': {}
-
- '@rollup/pluginutils@5.3.0(rollup@4.55.2)':
- dependencies:
- '@types/estree': 1.0.8
- estree-walker: 2.0.2
- picomatch: 4.0.3
- optionalDependencies:
- rollup: 4.55.2
-
- '@rollup/rollup-android-arm-eabi@4.55.2':
- optional: true
-
- '@rollup/rollup-android-arm64@4.55.2':
- optional: true
-
- '@rollup/rollup-darwin-arm64@4.55.2':
- optional: true
-
- '@rollup/rollup-darwin-x64@4.55.2':
- optional: true
-
- '@rollup/rollup-freebsd-arm64@4.55.2':
- optional: true
-
- '@rollup/rollup-freebsd-x64@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-arm-gnueabihf@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-arm-musleabihf@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-arm64-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-arm64-musl@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-loong64-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-loong64-musl@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-ppc64-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-ppc64-musl@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-riscv64-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-riscv64-musl@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-s390x-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-x64-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-linux-x64-musl@4.55.2':
- optional: true
-
- '@rollup/rollup-openbsd-x64@4.55.2':
- optional: true
-
- '@rollup/rollup-openharmony-arm64@4.55.2':
- optional: true
-
- '@rollup/rollup-win32-arm64-msvc@4.55.2':
- optional: true
-
- '@rollup/rollup-win32-ia32-msvc@4.55.2':
- optional: true
-
- '@rollup/rollup-win32-x64-gnu@4.55.2':
- optional: true
-
- '@rollup/rollup-win32-x64-msvc@4.55.2':
- optional: true
-
- '@sindresorhus/base62@1.0.0': {}
-
- '@standard-schema/utils@0.3.0': {}
-
- '@stylistic/eslint-plugin@5.7.0(eslint@9.39.2(jiti@2.6.1))':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@typescript-eslint/types': 8.53.1
- eslint: 9.39.2(jiti@2.6.1)
- eslint-visitor-keys: 5.0.0
- espree: 11.1.0
- estraverse: 5.3.0
- picomatch: 4.0.3
-
- '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
-
- '@svgr/babel-preset@8.1.0(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.6)
- '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.6)
-
- '@svgr/core@8.1.0(typescript@5.9.3)':
- dependencies:
- '@babel/core': 7.28.6
- '@svgr/babel-preset': 8.1.0(@babel/core@7.28.6)
- camelcase: 6.3.0
- cosmiconfig: 8.3.6(typescript@5.9.3)
- snake-case: 3.0.4
- transitivePeerDependencies:
- - supports-color
- - typescript
-
- '@svgr/hast-util-to-babel-ast@8.0.0':
- dependencies:
- '@babel/types': 7.28.6
- entities: 4.5.0
-
- '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))':
- dependencies:
- '@babel/core': 7.28.6
- '@svgr/babel-preset': 8.1.0(@babel/core@7.28.6)
- '@svgr/core': 8.1.0(typescript@5.9.3)
- '@svgr/hast-util-to-babel-ast': 8.0.0
- svg-parser: 2.0.4
- transitivePeerDependencies:
- - supports-color
-
- '@tabler/icons-react@3.36.1(react@19.2.3)':
- dependencies:
- '@tabler/icons': 3.36.1
- react: 19.2.3
-
- '@tabler/icons@3.36.1': {}
-
- '@tailwindcss/node@4.1.18':
- dependencies:
- '@jridgewell/remapping': 2.3.5
- enhanced-resolve: 5.18.4
- jiti: 2.6.1
- lightningcss: 1.30.2
- magic-string: 0.30.21
- source-map-js: 1.2.1
- tailwindcss: 4.1.18
-
- '@tailwindcss/oxide-android-arm64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-darwin-arm64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-darwin-x64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-freebsd-x64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-x64-musl@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-wasm32-wasi@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
- optional: true
-
- '@tailwindcss/oxide@4.1.18':
- optionalDependencies:
- '@tailwindcss/oxide-android-arm64': 4.1.18
- '@tailwindcss/oxide-darwin-arm64': 4.1.18
- '@tailwindcss/oxide-darwin-x64': 4.1.18
- '@tailwindcss/oxide-freebsd-x64': 4.1.18
- '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
- '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
- '@tailwindcss/oxide-linux-arm64-musl': 4.1.18
- '@tailwindcss/oxide-linux-x64-gnu': 4.1.18
- '@tailwindcss/oxide-linux-x64-musl': 4.1.18
- '@tailwindcss/oxide-wasm32-wasi': 4.1.18
- '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
- '@tailwindcss/oxide-win32-x64-msvc': 4.1.18
-
- '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)':
- dependencies:
- postcss-selector-parser: 6.0.10
- tailwindcss: 4.1.18
-
- '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
- dependencies:
- '@tailwindcss/node': 4.1.18
- '@tailwindcss/oxide': 4.1.18
- tailwindcss: 4.1.18
- vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
-
- '@tanstack/devtools-event-client@0.4.0': {}
-
- '@tanstack/eslint-plugin-query@5.91.3(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- transitivePeerDependencies:
- - supports-color
- - typescript
-
- '@tanstack/form-core@0.42.1':
- dependencies:
- '@tanstack/store': 0.7.7
-
- '@tanstack/form-core@1.27.7':
- dependencies:
- '@tanstack/devtools-event-client': 0.4.0
- '@tanstack/pacer-lite': 0.1.1
- '@tanstack/store': 0.7.7
-
- '@tanstack/history@1.153.2': {}
-
- '@tanstack/pacer-lite@0.1.1': {}
-
- '@tanstack/query-core@5.90.19': {}
-
- '@tanstack/react-form@1.27.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@tanstack/form-core': 1.27.7
- '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- react: 19.2.3
- transitivePeerDependencies:
- - react-dom
-
- '@tanstack/react-query@5.90.19(react@19.2.3)':
- dependencies:
- '@tanstack/query-core': 5.90.19
- react: 19.2.3
-
- '@tanstack/react-router-devtools@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-devtools-core': 1.153.2(@tanstack/router-core@1.153.2)(csstype@3.2.3)
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- optionalDependencies:
- '@tanstack/router-core': 1.153.2
- transitivePeerDependencies:
- - csstype
-
- '@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@tanstack/history': 1.153.2
- '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-core': 1.153.2
- isbot: 5.1.33
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- tiny-invariant: 1.3.3
- tiny-warning: 1.0.3
-
- '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@tanstack/store': 0.8.0
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- use-sync-external-store: 1.6.0(react@19.2.3)
-
- '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@tanstack/table-core': 8.21.3
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
-
- '@tanstack/router-core@1.153.2':
- dependencies:
- '@tanstack/history': 1.153.2
- '@tanstack/store': 0.8.0
- cookie-es: 2.0.0
- seroval: 1.4.2
- seroval-plugins: 1.4.2(seroval@1.4.2)
- tiny-invariant: 1.3.3
- tiny-warning: 1.0.3
-
- '@tanstack/router-devtools-core@1.153.2(@tanstack/router-core@1.153.2)(csstype@3.2.3)':
- dependencies:
- '@tanstack/router-core': 1.153.2
- clsx: 2.1.1
- goober: 2.1.18(csstype@3.2.3)
- tiny-invariant: 1.3.3
- optionalDependencies:
- csstype: 3.2.3
-
- '@tanstack/router-generator@1.153.2':
- dependencies:
- '@tanstack/router-core': 1.153.2
- '@tanstack/router-utils': 1.143.11
- '@tanstack/virtual-file-routes': 1.145.4
- prettier: 3.8.0
- recast: 0.23.11
- source-map: 0.7.6
- tsx: 4.21.0
- zod: 3.25.76
- transitivePeerDependencies:
- - supports-color
-
- '@tanstack/router-plugin@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6)
- '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.28.6)
- '@babel/template': 7.28.6
- '@babel/traverse': 7.28.6
- '@babel/types': 7.28.6
- '@tanstack/router-core': 1.153.2
- '@tanstack/router-generator': 1.153.2
- '@tanstack/router-utils': 1.143.11
- '@tanstack/virtual-file-routes': 1.145.4
- babel-dead-code-elimination: 1.0.12
- chokidar: 3.6.0
- unplugin: 2.3.11
- zod: 3.25.76
- optionalDependencies:
- '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
- transitivePeerDependencies:
- - supports-color
-
- '@tanstack/router-utils@1.143.11':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/generator': 7.28.6
- '@babel/parser': 7.28.6
- ansis: 4.2.0
- diff: 8.0.3
- pathe: 2.0.3
- tinyglobby: 0.2.15
- transitivePeerDependencies:
- - supports-color
-
- '@tanstack/store@0.7.7': {}
-
- '@tanstack/store@0.8.0': {}
-
- '@tanstack/table-core@8.21.3': {}
-
- '@tanstack/virtual-file-routes@1.145.4': {}
-
- '@tanstack/zod-adapter@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.5)':
- dependencies:
- '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- zod: 4.3.5
-
- '@tanstack/zod-form-adapter@0.42.1(zod@4.3.5)':
- dependencies:
- '@tanstack/form-core': 0.42.1
- zod: 4.3.5
-
- '@types/babel__core@7.20.5':
- dependencies:
- '@babel/parser': 7.28.6
- '@babel/types': 7.28.6
- '@types/babel__generator': 7.27.0
- '@types/babel__template': 7.4.4
- '@types/babel__traverse': 7.28.0
-
- '@types/babel__generator@7.27.0':
- dependencies:
- '@babel/types': 7.28.6
-
- '@types/babel__template@7.4.4':
- dependencies:
- '@babel/parser': 7.28.6
- '@babel/types': 7.28.6
-
- '@types/babel__traverse@7.28.0':
- dependencies:
- '@babel/types': 7.28.6
-
- '@types/base-64@1.0.2': {}
-
- '@types/culori@4.0.1': {}
-
- '@types/d3-array@3.2.2': {}
-
- '@types/d3-color@3.1.3': {}
-
- '@types/d3-ease@3.0.2': {}
-
- '@types/d3-interpolate@3.0.4':
- dependencies:
- '@types/d3-color': 3.1.3
-
- '@types/d3-path@3.1.1': {}
-
- '@types/d3-scale@4.0.9':
- dependencies:
- '@types/d3-time': 3.0.4
-
- '@types/d3-shape@3.1.8':
- dependencies:
- '@types/d3-path': 3.1.1
-
- '@types/d3-time@3.0.4': {}
-
- '@types/d3-timer@3.0.2': {}
-
- '@types/debug@4.1.12':
- dependencies:
- '@types/ms': 2.1.0
-
- '@types/estree-jsx@1.0.5':
- dependencies:
- '@types/estree': 1.0.8
-
- '@types/estree@1.0.8': {}
-
- '@types/hast@2.3.10':
- dependencies:
- '@types/unist': 2.0.11
-
- '@types/hast@3.0.4':
- dependencies:
- '@types/unist': 3.0.3
-
- '@types/json-schema@7.0.15': {}
-
- '@types/lodash-es@4.17.12':
- dependencies:
- '@types/lodash': 4.17.23
-
- '@types/lodash@4.17.23': {}
-
- '@types/mdast@4.0.4':
- dependencies:
- '@types/unist': 3.0.3
-
- '@types/ms@2.1.0': {}
-
- '@types/node@25.0.9':
- dependencies:
- undici-types: 7.16.0
-
- '@types/prismjs@1.26.5': {}
-
- '@types/qrcode@1.5.6':
- dependencies:
- '@types/node': 25.0.9
-
- '@types/react-dom@19.2.3(@types/react@19.2.8)':
- dependencies:
- '@types/react': 19.2.8
-
- '@types/react@19.2.8':
- dependencies:
- csstype: 3.2.3
-
- '@types/unist@2.0.11': {}
-
- '@types/unist@3.0.3': {}
-
- '@types/utf8@3.0.3': {}
-
- '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.53.1
- eslint: 9.39.2(jiti@2.6.1)
- ignore: 7.0.5
- natural-compare: 1.4.0
- ts-api-utils: 2.4.0(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.53.1
- debug: 4.4.3
- eslint: 9.39.2(jiti@2.6.1)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/types': 8.53.1
- debug: 4.4.3
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/scope-manager@8.53.1':
- dependencies:
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/visitor-keys': 8.53.1
-
- '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)':
- dependencies:
- typescript: 5.9.3
-
- '@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- debug: 4.4.3
- eslint: 9.39.2(jiti@2.6.1)
- ts-api-utils: 2.4.0(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/types@8.53.1': {}
-
- '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/visitor-keys': 8.53.1
- debug: 4.4.3
- minimatch: 9.0.5
- semver: 7.7.3
- tinyglobby: 0.2.15
- ts-api-utils: 2.4.0(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@typescript-eslint/visitor-keys@8.53.1':
- dependencies:
- '@typescript-eslint/types': 8.53.1
- eslint-visitor-keys: 4.2.1
-
- '@uiw/copy-to-clipboard@1.0.19': {}
-
- '@uiw/react-markdown-preview@5.1.5(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@babel/runtime': 7.28.6
- '@uiw/copy-to-clipboard': 1.0.19
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-markdown: 9.0.3(@types/react@19.2.8)(react@19.2.3)
- rehype-attr: 3.0.3
- rehype-autolink-headings: 7.1.0
- rehype-ignore: 2.0.3
- rehype-prism-plus: 2.0.0
- rehype-raw: 7.0.0
- rehype-rewrite: 4.0.4
- rehype-slug: 6.0.0
- remark-gfm: 4.0.1
- remark-github-blockquote-alert: 1.3.1
- unist-util-visit: 5.0.0
- transitivePeerDependencies:
- - '@types/react'
- - supports-color
-
- '@uiw/react-md-editor@4.0.11(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
- dependencies:
- '@babel/runtime': 7.28.6
- '@uiw/react-markdown-preview': 5.1.5(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- rehype: 13.0.2
- rehype-prism-plus: 2.0.1
- transitivePeerDependencies:
- - '@types/react'
- - supports-color
-
- '@ungap/structured-clone@1.3.0': {}
-
- '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6)
- '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6)
- '@rolldown/pluginutils': 1.0.0-beta.53
- '@types/babel__core': 7.20.5
- react-refresh: 0.18.0
- vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
- transitivePeerDependencies:
- - supports-color
-
- '@vitest/eslint-plugin@1.6.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- optionalDependencies:
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- '@vue/compiler-core@3.5.27':
- dependencies:
- '@babel/parser': 7.28.6
- '@vue/shared': 3.5.27
- entities: 7.0.0
- estree-walker: 2.0.2
- source-map-js: 1.2.1
-
- '@vue/compiler-dom@3.5.27':
- dependencies:
- '@vue/compiler-core': 3.5.27
- '@vue/shared': 3.5.27
-
- '@vue/compiler-sfc@3.5.27':
- dependencies:
- '@babel/parser': 7.28.6
- '@vue/compiler-core': 3.5.27
- '@vue/compiler-dom': 3.5.27
- '@vue/compiler-ssr': 3.5.27
- '@vue/shared': 3.5.27
- estree-walker: 2.0.2
- magic-string: 0.30.21
- postcss: 8.5.6
- source-map-js: 1.2.1
-
- '@vue/compiler-ssr@3.5.27':
- dependencies:
- '@vue/compiler-dom': 3.5.27
- '@vue/shared': 3.5.27
-
- '@vue/shared@3.5.27': {}
-
- acorn-jsx@5.3.2(acorn@8.15.0):
- dependencies:
- acorn: 8.15.0
-
- acorn@8.15.0: {}
-
- ajv@6.12.6:
- dependencies:
- fast-deep-equal: 3.1.3
- fast-json-stable-stringify: 2.1.0
- json-schema-traverse: 0.4.1
- uri-js: 4.4.1
-
- ansi-escapes@7.2.0:
- dependencies:
- environment: 1.1.0
-
- ansi-regex@5.0.1: {}
-
- ansi-regex@6.2.2: {}
-
- ansi-styles@4.3.0:
- dependencies:
- color-convert: 2.0.1
-
- ansi-styles@6.2.3: {}
-
- ansis@4.2.0: {}
-
- anymatch@3.1.3:
- dependencies:
- normalize-path: 3.0.0
- picomatch: 2.3.1
-
- are-docs-informative@0.0.2: {}
-
- argparse@2.0.1: {}
-
- aria-hidden@1.2.6:
- dependencies:
- tslib: 2.8.1
-
- ast-types@0.16.1:
- dependencies:
- tslib: 2.8.1
-
- asynckit@0.4.0: {}
-
- axios@1.13.2:
- dependencies:
- follow-redirects: 1.15.11
- form-data: 4.0.5
- proxy-from-env: 1.1.0
- transitivePeerDependencies:
- - debug
-
- babel-dead-code-elimination@1.0.12:
- dependencies:
- '@babel/core': 7.28.6
- '@babel/parser': 7.28.6
- '@babel/traverse': 7.28.6
- '@babel/types': 7.28.6
- transitivePeerDependencies:
- - supports-color
-
- bail@2.0.2: {}
-
- balanced-match@1.0.2: {}
-
- base-64@1.0.0: {}
-
- base64-js@1.5.1: {}
-
- baseline-browser-mapping@2.9.15: {}
-
- bcp-47-match@2.0.3: {}
-
- binary-extensions@2.3.0: {}
-
- birecord@0.1.1: {}
-
- boolbase@1.0.0: {}
-
- brace-expansion@1.1.12:
- dependencies:
- balanced-match: 1.0.2
- concat-map: 0.0.1
-
- brace-expansion@2.0.2:
- dependencies:
- balanced-match: 1.0.2
-
- braces@3.0.3:
- dependencies:
- fill-range: 7.1.1
-
- browserslist@4.28.1:
- dependencies:
- baseline-browser-mapping: 2.9.15
- caniuse-lite: 1.0.30001765
- electron-to-chromium: 1.5.267
- node-releases: 2.0.27
- update-browserslist-db: 1.2.3(browserslist@4.28.1)
-
- buffer@6.0.3:
- dependencies:
- base64-js: 1.5.1
- ieee754: 1.2.1
-
- builtin-modules@5.0.0: {}
-
- cac@6.7.14: {}
-
- call-bind-apply-helpers@1.0.2:
- dependencies:
- es-errors: 1.3.0
- function-bind: 1.1.2
-
- callsites@3.1.0: {}
-
- camelcase@5.3.1: {}
-
- camelcase@6.3.0: {}
-
- caniuse-lite@1.0.30001765: {}
-
- ccount@2.0.1: {}
-
- chalk@4.1.2:
- dependencies:
- ansi-styles: 4.3.0
- supports-color: 7.2.0
-
- change-case@5.4.4: {}
-
- character-entities-html4@2.1.0: {}
-
- character-entities-legacy@3.0.0: {}
-
- character-entities@2.0.2: {}
-
- character-reference-invalid@2.0.1: {}
-
- chokidar@3.6.0:
- dependencies:
- anymatch: 3.1.3
- braces: 3.0.3
- glob-parent: 5.1.2
- is-binary-path: 2.1.0
- is-glob: 4.0.3
- normalize-path: 3.0.0
- readdirp: 3.6.0
- optionalDependencies:
- fsevents: 2.3.3
-
- ci-info@4.3.1: {}
-
- class-variance-authority@0.7.1:
- dependencies:
- clsx: 2.1.1
-
- clean-regexp@1.0.0:
- dependencies:
- escape-string-regexp: 1.0.5
-
- cli-cursor@5.0.0:
- dependencies:
- restore-cursor: 5.1.0
-
- cli-truncate@5.1.1:
- dependencies:
- slice-ansi: 7.1.2
- string-width: 8.1.0
-
- cliui@6.0.0:
- dependencies:
- string-width: 4.2.3
- strip-ansi: 6.0.1
- wrap-ansi: 6.2.0
-
- clsx@2.1.1: {}
-
- color-convert@2.0.1:
- dependencies:
- color-name: 1.1.4
-
- color-name@1.1.4: {}
-
- colorette@2.0.20: {}
-
- combined-stream@1.0.8:
- dependencies:
- delayed-stream: 1.0.0
-
- comma-separated-tokens@2.0.3: {}
-
- commander@14.0.2: {}
-
- comment-parser@1.4.1: {}
-
- comment-parser@1.4.4: {}
-
- compare-versions@6.1.1: {}
-
- concat-map@0.0.1: {}
-
- confbox@0.1.8: {}
-
- confbox@0.2.2: {}
-
- convert-source-map@2.0.0: {}
-
- cookie-es@2.0.0: {}
-
- core-js-compat@3.47.0:
- dependencies:
- browserslist: 4.28.1
-
- cosmiconfig@8.3.6(typescript@5.9.3):
- dependencies:
- import-fresh: 3.3.1
- js-yaml: 4.1.1
- parse-json: 5.2.0
- path-type: 4.0.0
- optionalDependencies:
- typescript: 5.9.3
-
- cross-spawn@7.0.6:
- dependencies:
- path-key: 3.1.1
- shebang-command: 2.0.0
- which: 2.0.2
-
- css-selector-parser@3.3.0: {}
-
- cssesc@3.0.0: {}
-
- csstype@3.2.3: {}
-
- culori@4.0.2: {}
-
- d3-array@3.2.4:
- dependencies:
- internmap: 2.0.3
-
- d3-color@3.1.0: {}
-
- d3-ease@3.0.1: {}
-
- d3-format@3.1.2: {}
-
- d3-interpolate@3.0.1:
- dependencies:
- d3-color: 3.1.0
-
- d3-path@3.1.0: {}
-
- d3-scale@4.0.2:
- dependencies:
- d3-array: 3.2.4
- d3-format: 3.1.2
- d3-interpolate: 3.0.1
- d3-time: 3.1.0
- d3-time-format: 4.1.0
-
- d3-shape@3.2.0:
- dependencies:
- d3-path: 3.1.0
-
- d3-time-format@4.1.0:
- dependencies:
- d3-time: 3.1.0
-
- d3-time@3.1.0:
- dependencies:
- d3-array: 3.2.4
-
- d3-timer@3.0.1: {}
-
- debug@4.4.3:
- dependencies:
- ms: 2.1.3
-
- decamelize@1.2.0: {}
-
- decimal.js-light@2.5.1: {}
-
- decode-named-character-reference@1.3.0:
- dependencies:
- character-entities: 2.0.2
-
- deep-is@0.1.4: {}
-
- delayed-stream@1.0.0: {}
-
- dequal@2.0.3: {}
-
- detect-libc@2.1.2: {}
-
- detect-node-es@1.1.0: {}
-
- devlop@1.1.0:
- dependencies:
- dequal: 2.0.3
-
- diff-sequences@27.5.1: {}
-
- diff@8.0.3: {}
-
- dijkstrajs@1.0.3: {}
-
- direction@2.0.1: {}
-
- dom-helpers@5.2.1:
- dependencies:
- '@babel/runtime': 7.28.6
- csstype: 3.2.3
-
- dot-case@3.0.4:
- dependencies:
- no-case: 3.0.4
- tslib: 2.8.1
-
- dunder-proto@1.0.1:
- dependencies:
- call-bind-apply-helpers: 1.0.2
- es-errors: 1.3.0
- gopd: 1.2.0
-
- electron-to-chromium@1.5.267: {}
-
- emoji-regex@10.6.0: {}
-
- emoji-regex@8.0.0: {}
-
- empathic@2.0.0: {}
-
- enhanced-resolve@5.18.4:
- dependencies:
- graceful-fs: 4.2.11
- tapable: 2.3.0
-
- entities@4.5.0: {}
-
- entities@6.0.1: {}
-
- entities@7.0.0: {}
-
- environment@1.1.0: {}
-
- error-ex@1.3.4:
- dependencies:
- is-arrayish: 0.2.1
-
- es-define-property@1.0.1: {}
-
- es-errors@1.3.0: {}
-
- es-object-atoms@1.1.1:
- dependencies:
- es-errors: 1.3.0
-
- es-set-tostringtag@2.1.0:
- dependencies:
- es-errors: 1.3.0
- get-intrinsic: 1.3.0
- has-tostringtag: 1.0.2
- hasown: 2.0.2
-
- esbuild@0.27.2:
- optionalDependencies:
- '@esbuild/aix-ppc64': 0.27.2
- '@esbuild/android-arm': 0.27.2
- '@esbuild/android-arm64': 0.27.2
- '@esbuild/android-x64': 0.27.2
- '@esbuild/darwin-arm64': 0.27.2
- '@esbuild/darwin-x64': 0.27.2
- '@esbuild/freebsd-arm64': 0.27.2
- '@esbuild/freebsd-x64': 0.27.2
- '@esbuild/linux-arm': 0.27.2
- '@esbuild/linux-arm64': 0.27.2
- '@esbuild/linux-ia32': 0.27.2
- '@esbuild/linux-loong64': 0.27.2
- '@esbuild/linux-mips64el': 0.27.2
- '@esbuild/linux-ppc64': 0.27.2
- '@esbuild/linux-riscv64': 0.27.2
- '@esbuild/linux-s390x': 0.27.2
- '@esbuild/linux-x64': 0.27.2
- '@esbuild/netbsd-arm64': 0.27.2
- '@esbuild/netbsd-x64': 0.27.2
- '@esbuild/openbsd-arm64': 0.27.2
- '@esbuild/openbsd-x64': 0.27.2
- '@esbuild/openharmony-arm64': 0.27.2
- '@esbuild/sunos-x64': 0.27.2
- '@esbuild/win32-arm64': 0.27.2
- '@esbuild/win32-ia32': 0.27.2
- '@esbuild/win32-x64': 0.27.2
-
- escalade@3.2.0: {}
-
- escape-string-regexp@1.0.5: {}
-
- escape-string-regexp@4.0.0: {}
-
- escape-string-regexp@5.0.0: {}
-
- eslint-compat-utils@0.5.1(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
- semver: 7.7.3
-
- eslint-compat-utils@0.6.5(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
- semver: 7.7.3
-
- eslint-config-flat-gitignore@2.1.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1))
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-flat-config-utils@2.1.4:
- dependencies:
- pathe: 2.0.3
-
- eslint-json-compat-utils@0.2.1(eslint@9.39.2(jiti@2.6.1))(jsonc-eslint-parser@2.4.2):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
- esquery: 1.7.0
- jsonc-eslint-parser: 2.4.2
-
- eslint-merge-processors@2.0.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-plugin-antfu@3.1.3(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-plugin-command@3.4.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@es-joy/jsdoccomment': 0.78.0
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-plugin-es-x@7.8.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@eslint-community/regexpp': 4.12.2
- eslint: 9.39.2(jiti@2.6.1)
- eslint-compat-utils: 0.5.1(eslint@9.39.2(jiti@2.6.1))
-
- eslint-plugin-import-lite@0.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
- optionalDependencies:
- typescript: 5.9.3
-
- eslint-plugin-jsdoc@61.7.1(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@es-joy/jsdoccomment': 0.78.0
- '@es-joy/resolve.exports': 1.2.0
- are-docs-informative: 0.0.2
- comment-parser: 1.4.1
- debug: 4.4.3
- escape-string-regexp: 4.0.0
- eslint: 9.39.2(jiti@2.6.1)
- espree: 11.1.0
- esquery: 1.7.0
- html-entities: 2.6.0
- object-deep-merge: 2.0.0
- parse-imports-exports: 0.2.4
- semver: 7.7.3
- spdx-expression-parse: 4.0.0
- to-valid-identifier: 1.0.0
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-jsonc@2.21.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- diff-sequences: 27.5.1
- eslint: 9.39.2(jiti@2.6.1)
- eslint-compat-utils: 0.6.5(eslint@9.39.2(jiti@2.6.1))
- eslint-json-compat-utils: 0.2.1(eslint@9.39.2(jiti@2.6.1))(jsonc-eslint-parser@2.4.2)
- espree: 10.4.0
- graphemer: 1.4.0
- jsonc-eslint-parser: 2.4.2
- natural-compare: 1.4.0
- synckit: 0.11.12
- transitivePeerDependencies:
- - '@eslint/json'
-
- eslint-plugin-n@17.23.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- enhanced-resolve: 5.18.4
- eslint: 9.39.2(jiti@2.6.1)
- eslint-plugin-es-x: 7.8.0(eslint@9.39.2(jiti@2.6.1))
- get-tsconfig: 4.13.0
- globals: 15.15.0
- globrex: 0.1.2
- ignore: 5.3.2
- semver: 7.7.3
- ts-declaration-location: 1.0.7(typescript@5.9.3)
- transitivePeerDependencies:
- - typescript
-
- eslint-plugin-no-only-tests@3.3.0: {}
-
- eslint-plugin-perfectionist@4.15.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- natural-orderby: 5.0.0
- transitivePeerDependencies:
- - supports-color
- - typescript
-
- eslint-plugin-pnpm@1.5.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- empathic: 2.0.0
- eslint: 9.39.2(jiti@2.6.1)
- jsonc-eslint-parser: 2.4.2
- pathe: 2.0.3
- pnpm-workspace-yaml: 1.5.0
- tinyglobby: 0.2.15
- yaml: 2.8.2
- yaml-eslint-parser: 2.0.0
-
- eslint-plugin-react-dom@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/core': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/var': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- compare-versions: 6.1.1
- eslint: 9.39.2(jiti@2.6.1)
- string-ts: 2.3.1
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-react-hooks-extra@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/core': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/var': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- string-ts: 2.3.1
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@babel/core': 7.28.6
- '@babel/parser': 7.28.6
- eslint: 9.39.2(jiti@2.6.1)
- hermes-parser: 0.25.1
- zod: 4.3.5
- zod-validation-error: 4.0.2(zod@4.3.5)
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-react-naming-convention@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/core': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/var': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- compare-versions: 6.1.1
- eslint: 9.39.2(jiti@2.6.1)
- string-ts: 2.3.1
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-plugin-react-web-api@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/core': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/var': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- string-ts: 2.3.1
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-react-x@2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@eslint-react/ast': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/core': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/eff': 2.7.2
- '@eslint-react/shared': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@eslint-react/var': 2.7.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.53.1
- '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/types': 8.53.1
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- compare-versions: 6.1.1
- eslint: 9.39.2(jiti@2.6.1)
- is-immutable-type: 5.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- string-ts: 2.3.1
- ts-api-utils: 2.4.0(typescript@5.9.3)
- ts-pattern: 5.9.0
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@eslint-community/regexpp': 4.12.2
- comment-parser: 1.4.4
- eslint: 9.39.2(jiti@2.6.1)
- jsdoc-type-pratt-parser: 4.8.0
- refa: 0.12.1
- regexp-ast-analysis: 0.7.1
- scslre: 0.3.0
-
- eslint-plugin-toml@0.12.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- debug: 4.4.3
- eslint: 9.39.2(jiti@2.6.1)
- eslint-compat-utils: 0.6.5(eslint@9.39.2(jiti@2.6.1))
- lodash: 4.17.21
- toml-eslint-parser: 0.10.1
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@babel/helper-validator-identifier': 7.28.5
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@eslint/plugin-kit': 0.4.1
- change-case: 5.4.4
- ci-info: 4.3.1
- clean-regexp: 1.0.0
- core-js-compat: 3.47.0
- eslint: 9.39.2(jiti@2.6.1)
- esquery: 1.7.0
- find-up-simple: 1.0.1
- globals: 16.5.0
- indent-string: 5.0.0
- is-builtin-module: 5.0.0
- jsesc: 3.1.0
- pluralize: 8.0.0
- regexp-tree: 0.1.27
- regjsparser: 0.13.0
- semver: 7.7.3
- strip-indent: 4.1.1
-
- eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
- optionalDependencies:
- '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
-
- eslint-plugin-vue@10.7.0(@stylistic/eslint-plugin@5.7.0(eslint@9.39.2(jiti@2.6.1)))(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1))):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- eslint: 9.39.2(jiti@2.6.1)
- natural-compare: 1.4.0
- nth-check: 2.1.1
- postcss-selector-parser: 7.1.1
- semver: 7.7.3
- vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@2.6.1))
- xml-name-validator: 4.0.0
- optionalDependencies:
- '@stylistic/eslint-plugin': 5.7.0(eslint@9.39.2(jiti@2.6.1))
- '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
-
- eslint-plugin-yml@1.19.1(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- debug: 4.4.3
- diff-sequences: 27.5.1
- escape-string-regexp: 4.0.0
- eslint: 9.39.2(jiti@2.6.1)
- eslint-compat-utils: 0.6.5(eslint@9.39.2(jiti@2.6.1))
- natural-compare: 1.4.0
- yaml-eslint-parser: 1.3.2
- transitivePeerDependencies:
- - supports-color
-
- eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.27)(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@vue/compiler-sfc': 3.5.27
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-scope@8.4.0:
- dependencies:
- esrecurse: 4.3.0
- estraverse: 5.3.0
-
- eslint-visitor-keys@3.4.3: {}
-
- eslint-visitor-keys@4.2.1: {}
-
- eslint-visitor-keys@5.0.0: {}
-
- eslint@9.39.2(jiti@2.6.1):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@eslint-community/regexpp': 4.12.2
- '@eslint/config-array': 0.21.1
- '@eslint/config-helpers': 0.4.2
- '@eslint/core': 0.17.0
- '@eslint/eslintrc': 3.3.3
- '@eslint/js': 9.39.2
- '@eslint/plugin-kit': 0.4.1
- '@humanfs/node': 0.16.7
- '@humanwhocodes/module-importer': 1.0.1
- '@humanwhocodes/retry': 0.4.3
- '@types/estree': 1.0.8
- ajv: 6.12.6
- chalk: 4.1.2
- cross-spawn: 7.0.6
- debug: 4.4.3
- escape-string-regexp: 4.0.0
- eslint-scope: 8.4.0
- eslint-visitor-keys: 4.2.1
- espree: 10.4.0
- esquery: 1.7.0
- esutils: 2.0.3
- fast-deep-equal: 3.1.3
- file-entry-cache: 8.0.0
- find-up: 5.0.0
- glob-parent: 6.0.2
- ignore: 5.3.2
- imurmurhash: 0.1.4
- is-glob: 4.0.3
- json-stable-stringify-without-jsonify: 1.0.1
- lodash.merge: 4.6.2
- minimatch: 3.1.2
- natural-compare: 1.4.0
- optionator: 0.9.4
- optionalDependencies:
- jiti: 2.6.1
- transitivePeerDependencies:
- - supports-color
-
- espree@10.4.0:
- dependencies:
- acorn: 8.15.0
- acorn-jsx: 5.3.2(acorn@8.15.0)
- eslint-visitor-keys: 4.2.1
-
- espree@11.1.0:
- dependencies:
- acorn: 8.15.0
- acorn-jsx: 5.3.2(acorn@8.15.0)
- eslint-visitor-keys: 5.0.0
-
- espree@9.6.1:
- dependencies:
- acorn: 8.15.0
- acorn-jsx: 5.3.2(acorn@8.15.0)
- eslint-visitor-keys: 3.4.3
-
- esprima@4.0.1: {}
-
- esquery@1.7.0:
- dependencies:
- estraverse: 5.3.0
-
- esrecurse@4.3.0:
- dependencies:
- estraverse: 5.3.0
-
- estraverse@5.3.0: {}
-
- estree-util-is-identifier-name@3.0.0: {}
-
- estree-walker@2.0.2: {}
-
- esutils@2.0.3: {}
-
- eventemitter3@4.0.7: {}
-
- eventemitter3@5.0.4: {}
-
- exsolve@1.0.8: {}
-
- extend@3.0.2: {}
-
- fast-deep-equal@3.1.3: {}
-
- fast-equals@5.4.0: {}
-
- fast-json-stable-stringify@2.1.0: {}
-
- fast-levenshtein@2.0.6: {}
-
- fault@2.0.1:
- dependencies:
- format: 0.2.2
-
- fdir@6.5.0(picomatch@4.0.3):
- optionalDependencies:
- picomatch: 4.0.3
-
- file-entry-cache@8.0.0:
- dependencies:
- flat-cache: 4.0.1
-
- fill-range@7.1.1:
- dependencies:
- to-regex-range: 5.0.1
-
- find-up-simple@1.0.1: {}
-
- find-up@4.1.0:
- dependencies:
- locate-path: 5.0.0
- path-exists: 4.0.0
-
- find-up@5.0.0:
- dependencies:
- locate-path: 6.0.0
- path-exists: 4.0.0
-
- flat-cache@4.0.1:
- dependencies:
- flatted: 3.3.3
- keyv: 4.5.4
-
- flatted@3.3.3: {}
-
- follow-redirects@1.15.11: {}
-
- form-data@4.0.5:
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- es-set-tostringtag: 2.1.0
- hasown: 2.0.2
- mime-types: 2.1.35
-
- format@0.2.2: {}
-
- fsevents@2.3.3:
- optional: true
-
- function-bind@1.1.2: {}
-
- gensync@1.0.0-beta.2: {}
-
- get-caller-file@2.0.5: {}
-
- get-east-asian-width@1.4.0: {}
-
- get-intrinsic@1.3.0:
- dependencies:
- call-bind-apply-helpers: 1.0.2
- es-define-property: 1.0.1
- es-errors: 1.3.0
- es-object-atoms: 1.1.1
- function-bind: 1.1.2
- get-proto: 1.0.1
- gopd: 1.2.0
- has-symbols: 1.1.0
- hasown: 2.0.2
- math-intrinsics: 1.1.0
-
- get-nonce@1.0.1: {}
-
- get-proto@1.0.1:
- dependencies:
- dunder-proto: 1.0.1
- es-object-atoms: 1.1.1
-
- get-tsconfig@4.13.0:
- dependencies:
- resolve-pkg-maps: 1.0.0
-
- github-slugger@2.0.0: {}
-
- glob-parent@5.1.2:
- dependencies:
- is-glob: 4.0.3
-
- glob-parent@6.0.2:
- dependencies:
- is-glob: 4.0.3
-
- globals@14.0.0: {}
-
- globals@15.15.0: {}
-
- globals@16.5.0: {}
-
- globrex@0.1.2: {}
-
- goober@2.1.18(csstype@3.2.3):
- dependencies:
- csstype: 3.2.3
-
- gopd@1.2.0: {}
-
- graceful-fs@4.2.11: {}
-
- graphemer@1.4.0: {}
-
- has-flag@4.0.0: {}
-
- has-symbols@1.1.0: {}
-
- has-tostringtag@1.0.2:
- dependencies:
- has-symbols: 1.1.0
-
- hasown@2.0.2:
- dependencies:
- function-bind: 1.1.2
-
- hast-util-from-html@2.0.3:
- dependencies:
- '@types/hast': 3.0.4
- devlop: 1.1.0
- hast-util-from-parse5: 8.0.3
- parse5: 7.3.0
- vfile: 6.0.3
- vfile-message: 4.0.3
-
- hast-util-from-parse5@8.0.3:
- dependencies:
- '@types/hast': 3.0.4
- '@types/unist': 3.0.3
- devlop: 1.1.0
- hastscript: 9.0.1
- property-information: 7.1.0
- vfile: 6.0.3
- vfile-location: 5.0.3
- web-namespaces: 2.0.1
-
- hast-util-has-property@3.0.0:
- dependencies:
- '@types/hast': 3.0.4
-
- hast-util-heading-rank@3.0.0:
- dependencies:
- '@types/hast': 3.0.4
-
- hast-util-is-element@3.0.0:
- dependencies:
- '@types/hast': 3.0.4
-
- hast-util-parse-selector@3.1.1:
- dependencies:
- '@types/hast': 2.3.10
-
- hast-util-parse-selector@4.0.0:
- dependencies:
- '@types/hast': 3.0.4
-
- hast-util-raw@9.1.0:
- dependencies:
- '@types/hast': 3.0.4
- '@types/unist': 3.0.3
- '@ungap/structured-clone': 1.3.0
- hast-util-from-parse5: 8.0.3
- hast-util-to-parse5: 8.0.1
- html-void-elements: 3.0.0
- mdast-util-to-hast: 13.2.1
- parse5: 7.3.0
- unist-util-position: 5.0.0
- unist-util-visit: 5.0.0
- vfile: 6.0.3
- web-namespaces: 2.0.1
- zwitch: 2.0.4
-
- hast-util-select@6.0.4:
- dependencies:
- '@types/hast': 3.0.4
- '@types/unist': 3.0.3
- bcp-47-match: 2.0.3
- comma-separated-tokens: 2.0.3
- css-selector-parser: 3.3.0
- devlop: 1.1.0
- direction: 2.0.1
- hast-util-has-property: 3.0.0
- hast-util-to-string: 3.0.1
- hast-util-whitespace: 3.0.0
- nth-check: 2.1.1
- property-information: 7.1.0
- space-separated-tokens: 2.0.2
- unist-util-visit: 5.0.0
- zwitch: 2.0.4
-
- hast-util-to-html@9.0.5:
- dependencies:
- '@types/hast': 3.0.4
- '@types/unist': 3.0.3
- ccount: 2.0.1
- comma-separated-tokens: 2.0.3
- hast-util-whitespace: 3.0.0
- html-void-elements: 3.0.0
- mdast-util-to-hast: 13.2.1
- property-information: 7.1.0
- space-separated-tokens: 2.0.2
- stringify-entities: 4.0.4
- zwitch: 2.0.4
-
- hast-util-to-jsx-runtime@2.3.6:
- dependencies:
- '@types/estree': 1.0.8
- '@types/hast': 3.0.4
- '@types/unist': 3.0.3
- comma-separated-tokens: 2.0.3
- devlop: 1.1.0
- estree-util-is-identifier-name: 3.0.0
- hast-util-whitespace: 3.0.0
- mdast-util-mdx-expression: 2.0.1
- mdast-util-mdx-jsx: 3.2.0
- mdast-util-mdxjs-esm: 2.0.1
- property-information: 7.1.0
- space-separated-tokens: 2.0.2
- style-to-js: 1.1.21
- unist-util-position: 5.0.0
- vfile-message: 4.0.3
- transitivePeerDependencies:
- - supports-color
-
- hast-util-to-parse5@8.0.1:
- dependencies:
- '@types/hast': 3.0.4
- comma-separated-tokens: 2.0.3
- devlop: 1.1.0
- property-information: 7.1.0
- space-separated-tokens: 2.0.2
- web-namespaces: 2.0.1
- zwitch: 2.0.4
-
- hast-util-to-string@3.0.1:
- dependencies:
- '@types/hast': 3.0.4
-
- hast-util-whitespace@3.0.0:
- dependencies:
- '@types/hast': 3.0.4
-
- hastscript@7.2.0:
- dependencies:
- '@types/hast': 2.3.10
- comma-separated-tokens: 2.0.3
- hast-util-parse-selector: 3.1.1
- property-information: 6.5.0
- space-separated-tokens: 2.0.2
-
- hastscript@9.0.1:
- dependencies:
- '@types/hast': 3.0.4
- comma-separated-tokens: 2.0.3
- hast-util-parse-selector: 4.0.0
- property-information: 7.1.0
- space-separated-tokens: 2.0.2
-
- hermes-estree@0.25.1: {}
-
- hermes-parser@0.25.1:
- dependencies:
- hermes-estree: 0.25.1
-
- html-entities@2.6.0: {}
-
- html-url-attributes@3.0.1: {}
-
- html-void-elements@3.0.0: {}
-
- ieee754@1.2.1: {}
-
- ignore@5.3.2: {}
-
- ignore@7.0.5: {}
-
- immer@11.1.3: {}
-
- import-fresh@3.3.1:
- dependencies:
- parent-module: 1.0.1
- resolve-from: 4.0.0
-
- imurmurhash@0.1.4: {}
-
- indent-string@5.0.0: {}
-
- inline-style-parser@0.2.7: {}
-
- internmap@2.0.3: {}
-
- is-alphabetical@2.0.1: {}
-
- is-alphanumerical@2.0.1:
- dependencies:
- is-alphabetical: 2.0.1
- is-decimal: 2.0.1
-
- is-arrayish@0.2.1: {}
-
- is-binary-path@2.1.0:
- dependencies:
- binary-extensions: 2.3.0
-
- is-builtin-module@5.0.0:
- dependencies:
- builtin-modules: 5.0.0
-
- is-decimal@2.0.1: {}
-
- is-extglob@2.1.1: {}
-
- is-fullwidth-code-point@3.0.0: {}
-
- is-fullwidth-code-point@5.1.0:
- dependencies:
- get-east-asian-width: 1.4.0
-
- is-glob@4.0.3:
- dependencies:
- is-extglob: 2.1.1
-
- is-hexadecimal@2.0.1: {}
-
- is-immutable-type@5.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- ts-api-utils: 2.4.0(typescript@5.9.3)
- ts-declaration-location: 1.0.7(typescript@5.9.3)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- is-number@7.0.0: {}
-
- is-plain-obj@4.1.0: {}
-
- isbot@5.1.33: {}
-
- isexe@2.0.0: {}
-
- jiti@2.6.1: {}
-
- js-tokens@4.0.0: {}
-
- js-yaml@4.1.1:
- dependencies:
- argparse: 2.0.1
-
- jsdoc-type-pratt-parser@4.8.0: {}
-
- jsdoc-type-pratt-parser@7.0.0: {}
-
- jsesc@3.1.0: {}
-
- json-buffer@3.0.1: {}
-
- json-parse-even-better-errors@2.3.1: {}
-
- json-schema-traverse@0.4.1: {}
-
- json-stable-stringify-without-jsonify@1.0.1: {}
-
- json5@2.2.3: {}
-
- jsonc-eslint-parser@2.4.2:
- dependencies:
- acorn: 8.15.0
- eslint-visitor-keys: 3.4.3
- espree: 9.6.1
- semver: 7.7.3
-
- keyv@4.5.4:
- dependencies:
- json-buffer: 3.0.1
-
- levn@0.4.1:
- dependencies:
- prelude-ls: 1.2.1
- type-check: 0.4.0
-
- lightningcss-android-arm64@1.30.2:
- optional: true
-
- lightningcss-darwin-arm64@1.30.2:
- optional: true
-
- lightningcss-darwin-x64@1.30.2:
- optional: true
-
- lightningcss-freebsd-x64@1.30.2:
- optional: true
-
- lightningcss-linux-arm-gnueabihf@1.30.2:
- optional: true
-
- lightningcss-linux-arm64-gnu@1.30.2:
- optional: true
-
- lightningcss-linux-arm64-musl@1.30.2:
- optional: true
-
- lightningcss-linux-x64-gnu@1.30.2:
- optional: true
-
- lightningcss-linux-x64-musl@1.30.2:
- optional: true
-
- lightningcss-win32-arm64-msvc@1.30.2:
- optional: true
-
- lightningcss-win32-x64-msvc@1.30.2:
- optional: true
-
- lightningcss@1.30.2:
- dependencies:
- detect-libc: 2.1.2
- optionalDependencies:
- lightningcss-android-arm64: 1.30.2
- lightningcss-darwin-arm64: 1.30.2
- lightningcss-darwin-x64: 1.30.2
- lightningcss-freebsd-x64: 1.30.2
- lightningcss-linux-arm-gnueabihf: 1.30.2
- lightningcss-linux-arm64-gnu: 1.30.2
- lightningcss-linux-arm64-musl: 1.30.2
- lightningcss-linux-x64-gnu: 1.30.2
- lightningcss-linux-x64-musl: 1.30.2
- lightningcss-win32-arm64-msvc: 1.30.2
- lightningcss-win32-x64-msvc: 1.30.2
-
- lines-and-columns@1.2.4: {}
-
- lint-staged@16.2.7:
- dependencies:
- commander: 14.0.2
- listr2: 9.0.5
- micromatch: 4.0.8
- nano-spawn: 2.0.0
- pidtree: 0.6.0
- string-argv: 0.3.2
- yaml: 2.8.2
-
- listr2@9.0.5:
- dependencies:
- cli-truncate: 5.1.1
- colorette: 2.0.20
- eventemitter3: 5.0.4
- log-update: 6.1.0
- rfdc: 1.4.1
- wrap-ansi: 9.0.2
-
- local-pkg@1.1.2:
- dependencies:
- mlly: 1.8.0
- pkg-types: 2.3.0
- quansync: 0.2.11
-
- locate-path@5.0.0:
- dependencies:
- p-locate: 4.1.0
-
- locate-path@6.0.0:
- dependencies:
- p-locate: 5.0.0
-
- lodash-es@4.17.22: {}
-
- lodash.merge@4.6.2: {}
-
- lodash@4.17.21: {}
-
- log-update@6.1.0:
- dependencies:
- ansi-escapes: 7.2.0
- cli-cursor: 5.0.0
- slice-ansi: 7.1.2
- strip-ansi: 7.1.2
- wrap-ansi: 9.0.2
-
- longest-streak@3.1.0: {}
-
- loose-envify@1.4.0:
- dependencies:
- js-tokens: 4.0.0
-
- lower-case@2.0.2:
- dependencies:
- tslib: 2.8.1
-
- lru-cache@5.1.1:
- dependencies:
- yallist: 3.1.1
-
- lucide-react@0.562.0(react@19.2.3):
- dependencies:
- react: 19.2.3
-
- magic-string@0.30.21:
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
-
- markdown-table@3.0.4: {}
-
- math-intrinsics@1.1.0: {}
-
- mdast-util-find-and-replace@3.0.2:
- dependencies:
- '@types/mdast': 4.0.4
- escape-string-regexp: 5.0.0
- unist-util-is: 6.0.1
- unist-util-visit-parents: 6.0.2
-
- mdast-util-from-markdown@2.0.2:
- dependencies:
- '@types/mdast': 4.0.4
- '@types/unist': 3.0.3
- decode-named-character-reference: 1.3.0
- devlop: 1.1.0
- mdast-util-to-string: 4.0.0
- micromark: 4.0.2
- micromark-util-decode-numeric-character-reference: 2.0.2
- micromark-util-decode-string: 2.0.1
- micromark-util-normalize-identifier: 2.0.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
- unist-util-stringify-position: 4.0.0
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-frontmatter@2.0.1:
- dependencies:
- '@types/mdast': 4.0.4
- devlop: 1.1.0
- escape-string-regexp: 5.0.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- micromark-extension-frontmatter: 2.0.0
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-gfm-autolink-literal@2.0.1:
- dependencies:
- '@types/mdast': 4.0.4
- ccount: 2.0.1
- devlop: 1.1.0
- mdast-util-find-and-replace: 3.0.2
- micromark-util-character: 2.1.1
-
- mdast-util-gfm-footnote@2.1.0:
- dependencies:
- '@types/mdast': 4.0.4
- devlop: 1.1.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- micromark-util-normalize-identifier: 2.0.1
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-gfm-strikethrough@2.0.0:
- dependencies:
- '@types/mdast': 4.0.4
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-gfm-table@2.0.0:
- dependencies:
- '@types/mdast': 4.0.4
- devlop: 1.1.0
- markdown-table: 3.0.4
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-gfm-task-list-item@2.0.0:
- dependencies:
- '@types/mdast': 4.0.4
- devlop: 1.1.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-gfm@3.1.0:
- dependencies:
- mdast-util-from-markdown: 2.0.2
- mdast-util-gfm-autolink-literal: 2.0.1
- mdast-util-gfm-footnote: 2.1.0
- mdast-util-gfm-strikethrough: 2.0.0
- mdast-util-gfm-table: 2.0.0
- mdast-util-gfm-task-list-item: 2.0.0
- mdast-util-to-markdown: 2.1.2
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-mdx-expression@2.0.1:
- dependencies:
- '@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- devlop: 1.1.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-mdx-jsx@3.2.0:
- dependencies:
- '@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- '@types/unist': 3.0.3
- ccount: 2.0.1
- devlop: 1.1.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- parse-entities: 4.0.2
- stringify-entities: 4.0.4
- unist-util-stringify-position: 4.0.0
- vfile-message: 4.0.3
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-mdxjs-esm@2.0.1:
- dependencies:
- '@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- devlop: 1.1.0
- mdast-util-from-markdown: 2.0.2
- mdast-util-to-markdown: 2.1.2
- transitivePeerDependencies:
- - supports-color
-
- mdast-util-phrasing@4.1.0:
- dependencies:
- '@types/mdast': 4.0.4
- unist-util-is: 6.0.1
-
- mdast-util-to-hast@13.2.1:
- dependencies:
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- '@ungap/structured-clone': 1.3.0
- devlop: 1.1.0
- micromark-util-sanitize-uri: 2.0.1
- trim-lines: 3.0.1
- unist-util-position: 5.0.0
- unist-util-visit: 5.0.0
- vfile: 6.0.3
-
- mdast-util-to-markdown@2.1.2:
- dependencies:
- '@types/mdast': 4.0.4
- '@types/unist': 3.0.3
- longest-streak: 3.1.0
- mdast-util-phrasing: 4.1.0
- mdast-util-to-string: 4.0.0
- micromark-util-classify-character: 2.0.1
- micromark-util-decode-string: 2.0.1
- unist-util-visit: 5.0.0
- zwitch: 2.0.4
-
- mdast-util-to-string@4.0.0:
- dependencies:
- '@types/mdast': 4.0.4
-
- micromark-core-commonmark@2.0.3:
- dependencies:
- decode-named-character-reference: 1.3.0
- devlop: 1.1.0
- micromark-factory-destination: 2.0.1
- micromark-factory-label: 2.0.1
- micromark-factory-space: 2.0.1
- micromark-factory-title: 2.0.1
- micromark-factory-whitespace: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-chunked: 2.0.1
- micromark-util-classify-character: 2.0.1
- micromark-util-html-tag-name: 2.0.1
- micromark-util-normalize-identifier: 2.0.1
- micromark-util-resolve-all: 2.0.1
- micromark-util-subtokenize: 2.1.0
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-frontmatter@2.0.0:
- dependencies:
- fault: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm-autolink-literal@2.1.0:
- dependencies:
- micromark-util-character: 2.1.1
- micromark-util-sanitize-uri: 2.0.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm-footnote@2.1.0:
- dependencies:
- devlop: 1.1.0
- micromark-core-commonmark: 2.0.3
- micromark-factory-space: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-normalize-identifier: 2.0.1
- micromark-util-sanitize-uri: 2.0.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm-strikethrough@2.1.0:
- dependencies:
- devlop: 1.1.0
- micromark-util-chunked: 2.0.1
- micromark-util-classify-character: 2.0.1
- micromark-util-resolve-all: 2.0.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm-table@2.1.1:
- dependencies:
- devlop: 1.1.0
- micromark-factory-space: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm-tagfilter@2.0.0:
- dependencies:
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm-task-list-item@2.1.0:
- dependencies:
- devlop: 1.1.0
- micromark-factory-space: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-extension-gfm@3.0.0:
- dependencies:
- micromark-extension-gfm-autolink-literal: 2.1.0
- micromark-extension-gfm-footnote: 2.1.0
- micromark-extension-gfm-strikethrough: 2.1.0
- micromark-extension-gfm-table: 2.1.1
- micromark-extension-gfm-tagfilter: 2.0.0
- micromark-extension-gfm-task-list-item: 2.1.0
- micromark-util-combine-extensions: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-factory-destination@2.0.1:
- dependencies:
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-factory-label@2.0.1:
- dependencies:
- devlop: 1.1.0
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-factory-space@2.0.1:
- dependencies:
- micromark-util-character: 2.1.1
- micromark-util-types: 2.0.2
-
- micromark-factory-title@2.0.1:
- dependencies:
- micromark-factory-space: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-factory-whitespace@2.0.1:
- dependencies:
- micromark-factory-space: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-util-character@2.1.1:
- dependencies:
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-util-chunked@2.0.1:
- dependencies:
- micromark-util-symbol: 2.0.1
-
- micromark-util-classify-character@2.0.1:
- dependencies:
- micromark-util-character: 2.1.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-util-combine-extensions@2.0.1:
- dependencies:
- micromark-util-chunked: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-util-decode-numeric-character-reference@2.0.2:
- dependencies:
- micromark-util-symbol: 2.0.1
-
- micromark-util-decode-string@2.0.1:
- dependencies:
- decode-named-character-reference: 1.3.0
- micromark-util-character: 2.1.1
- micromark-util-decode-numeric-character-reference: 2.0.2
- micromark-util-symbol: 2.0.1
-
- micromark-util-encode@2.0.1: {}
-
- micromark-util-html-tag-name@2.0.1: {}
-
- micromark-util-normalize-identifier@2.0.1:
- dependencies:
- micromark-util-symbol: 2.0.1
-
- micromark-util-resolve-all@2.0.1:
- dependencies:
- micromark-util-types: 2.0.2
-
- micromark-util-sanitize-uri@2.0.1:
- dependencies:
- micromark-util-character: 2.1.1
- micromark-util-encode: 2.0.1
- micromark-util-symbol: 2.0.1
-
- micromark-util-subtokenize@2.1.0:
- dependencies:
- devlop: 1.1.0
- micromark-util-chunked: 2.0.1
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
-
- micromark-util-symbol@2.0.1: {}
-
- micromark-util-types@2.0.2: {}
-
- micromark@4.0.2:
- dependencies:
- '@types/debug': 4.1.12
- debug: 4.4.3
- decode-named-character-reference: 1.3.0
- devlop: 1.1.0
- micromark-core-commonmark: 2.0.3
- micromark-factory-space: 2.0.1
- micromark-util-character: 2.1.1
- micromark-util-chunked: 2.0.1
- micromark-util-combine-extensions: 2.0.1
- micromark-util-decode-numeric-character-reference: 2.0.2
- micromark-util-encode: 2.0.1
- micromark-util-normalize-identifier: 2.0.1
- micromark-util-resolve-all: 2.0.1
- micromark-util-sanitize-uri: 2.0.1
- micromark-util-subtokenize: 2.1.0
- micromark-util-symbol: 2.0.1
- micromark-util-types: 2.0.2
- transitivePeerDependencies:
- - supports-color
-
- micromatch@4.0.8:
- dependencies:
- braces: 3.0.3
- picomatch: 2.3.1
-
- mime-db@1.52.0: {}
-
- mime-types@2.1.35:
- dependencies:
- mime-db: 1.52.0
-
- mimic-function@5.0.1: {}
-
- minimatch@3.1.2:
- dependencies:
- brace-expansion: 1.1.12
-
- minimatch@9.0.5:
- dependencies:
- brace-expansion: 2.0.2
-
- mlly@1.8.0:
- dependencies:
- acorn: 8.15.0
- pathe: 2.0.3
- pkg-types: 1.3.1
- ufo: 1.6.3
-
- ms@2.1.3: {}
-
- nano-spawn@2.0.0: {}
-
- nanoid@3.3.11: {}
-
- natural-compare@1.4.0: {}
-
- natural-orderby@5.0.0: {}
-
- next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
- dependencies:
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
-
- no-case@3.0.4:
- dependencies:
- lower-case: 2.0.2
- tslib: 2.8.1
-
- node-releases@2.0.27: {}
-
- normalize-path@3.0.0: {}
-
- nth-check@2.1.1:
- dependencies:
- boolbase: 1.0.0
-
- object-assign@4.1.1: {}
-
- object-deep-merge@2.0.0: {}
-
- onetime@7.0.0:
- dependencies:
- mimic-function: 5.0.1
-
- optionator@0.9.4:
- dependencies:
- deep-is: 0.1.4
- fast-levenshtein: 2.0.6
- levn: 0.4.1
- prelude-ls: 1.2.1
- type-check: 0.4.0
- word-wrap: 1.2.5
-
- p-limit@2.3.0:
- dependencies:
- p-try: 2.2.0
-
- p-limit@3.1.0:
- dependencies:
- yocto-queue: 0.1.0
-
- p-locate@4.1.0:
- dependencies:
- p-limit: 2.3.0
-
- p-locate@5.0.0:
- dependencies:
- p-limit: 3.1.0
-
- p-try@2.2.0: {}
-
- package-manager-detector@1.6.0: {}
-
- parent-module@1.0.1:
- dependencies:
- callsites: 3.1.0
-
- parse-entities@4.0.2:
- dependencies:
- '@types/unist': 2.0.11
- character-entities-legacy: 3.0.0
- character-reference-invalid: 2.0.1
- decode-named-character-reference: 1.3.0
- is-alphanumerical: 2.0.1
- is-decimal: 2.0.1
- is-hexadecimal: 2.0.1
-
- parse-gitignore@2.0.0: {}
-
- parse-imports-exports@0.2.4:
- dependencies:
- parse-statements: 1.0.11
-
- parse-json@5.2.0:
- dependencies:
- '@babel/code-frame': 7.28.6
- error-ex: 1.3.4
- json-parse-even-better-errors: 2.3.1
- lines-and-columns: 1.2.4
-
- parse-numeric-range@1.3.0: {}
-
- parse-statements@1.0.11: {}
-
- parse5@7.3.0:
- dependencies:
- entities: 6.0.1
-
- path-exists@4.0.0: {}
-
- path-key@3.1.1: {}
-
- path-type@4.0.0: {}
-
- pathe@2.0.3: {}
-
- picocolors@1.1.1: {}
-
- picomatch@2.3.1: {}
-
- picomatch@4.0.3: {}
-
- pidtree@0.6.0: {}
-
- pkg-types@1.3.1:
- dependencies:
- confbox: 0.1.8
- mlly: 1.8.0
- pathe: 2.0.3
-
- pkg-types@2.3.0:
- dependencies:
- confbox: 0.2.2
- exsolve: 1.0.8
- pathe: 2.0.3
-
- pluralize@8.0.0: {}
-
- pngjs@5.0.0: {}
-
- pnpm-workspace-yaml@1.5.0:
- dependencies:
- yaml: 2.8.2
-
- postcss-selector-parser@6.0.10:
- dependencies:
- cssesc: 3.0.0
- util-deprecate: 1.0.2
-
- postcss-selector-parser@7.1.1:
- dependencies:
- cssesc: 3.0.0
- util-deprecate: 1.0.2
-
- postcss@8.5.6:
- dependencies:
- nanoid: 3.3.11
- picocolors: 1.1.1
- source-map-js: 1.2.1
-
- prelude-ls@1.2.1: {}
-
- prettier@3.8.0: {}
-
- prop-types@15.8.1:
- dependencies:
- loose-envify: 1.4.0
- object-assign: 4.1.1
- react-is: 16.13.1
-
- property-information@6.5.0: {}
-
- property-information@7.1.0: {}
-
- proxy-from-env@1.1.0: {}
-
- punycode@2.3.1: {}
-
- qrcode@1.5.4:
- dependencies:
- dijkstrajs: 1.0.3
- pngjs: 5.0.0
- yargs: 15.4.1
-
- quansync@0.2.11: {}
-
- react-dom@19.2.3(react@19.2.3):
- dependencies:
- react: 19.2.3
- scheduler: 0.27.0
-
- react-hook-form@7.71.1(react@19.2.3):
- dependencies:
- react: 19.2.3
-
- react-is@16.13.1: {}
-
- react-is@18.3.1: {}
-
- react-markdown@10.1.0(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- '@types/react': 19.2.8
- devlop: 1.1.0
- hast-util-to-jsx-runtime: 2.3.6
- html-url-attributes: 3.0.1
- mdast-util-to-hast: 13.2.1
- react: 19.2.3
- remark-parse: 11.0.0
- remark-rehype: 11.1.2
- unified: 11.0.5
- unist-util-visit: 5.0.0
- vfile: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- react-markdown@9.0.3(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- '@types/hast': 3.0.4
- '@types/react': 19.2.8
- devlop: 1.1.0
- hast-util-to-jsx-runtime: 2.3.6
- html-url-attributes: 3.0.1
- mdast-util-to-hast: 13.2.1
- react: 19.2.3
- remark-parse: 11.0.0
- remark-rehype: 11.1.2
- unified: 11.0.5
- unist-util-visit: 5.0.0
- vfile: 6.0.3
- transitivePeerDependencies:
- - supports-color
-
- react-refresh@0.18.0: {}
-
- react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- react: 19.2.3
- react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3)
- tslib: 2.8.1
- optionalDependencies:
- '@types/react': 19.2.8
-
- react-remove-scroll@2.7.2(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- react: 19.2.3
- react-remove-scroll-bar: 2.3.8(@types/react@19.2.8)(react@19.2.3)
- react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3)
- tslib: 2.8.1
- use-callback-ref: 1.3.3(@types/react@19.2.8)(react@19.2.3)
- use-sidecar: 1.1.3(@types/react@19.2.8)(react@19.2.3)
- optionalDependencies:
- '@types/react': 19.2.8
-
- react-smooth@4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
- dependencies:
- fast-equals: 5.4.0
- prop-types: 15.8.1
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-transition-group: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
-
- react-style-singleton@2.2.3(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- get-nonce: 1.0.1
- react: 19.2.3
- tslib: 2.8.1
- optionalDependencies:
- '@types/react': 19.2.8
-
- react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
- dependencies:
- '@babel/runtime': 7.28.6
- dom-helpers: 5.2.1
- loose-envify: 1.4.0
- prop-types: 15.8.1
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
-
- react@19.2.3: {}
-
- readdirp@3.6.0:
- dependencies:
- picomatch: 2.3.1
-
- recast@0.23.11:
- dependencies:
- ast-types: 0.16.1
- esprima: 4.0.1
- source-map: 0.6.1
- tiny-invariant: 1.3.3
- tslib: 2.8.1
-
- recharts-scale@0.4.5:
- dependencies:
- decimal.js-light: 2.5.1
-
- recharts@2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
- dependencies:
- clsx: 2.1.1
- eventemitter3: 4.0.7
- lodash: 4.17.21
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- react-is: 18.3.1
- react-smooth: 4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- recharts-scale: 0.4.5
- tiny-invariant: 1.3.3
- victory-vendor: 36.9.2
-
- refa@0.12.1:
- dependencies:
- '@eslint-community/regexpp': 4.12.2
-
- refractor@4.9.0:
- dependencies:
- '@types/hast': 2.3.10
- '@types/prismjs': 1.26.5
- hastscript: 7.2.0
- parse-entities: 4.0.2
-
- regexp-ast-analysis@0.7.1:
- dependencies:
- '@eslint-community/regexpp': 4.12.2
- refa: 0.12.1
-
- regexp-tree@0.1.27: {}
-
- regjsparser@0.13.0:
- dependencies:
- jsesc: 3.1.0
-
- rehype-attr@3.0.3:
- dependencies:
- unified: 11.0.5
- unist-util-visit: 5.0.0
-
- rehype-autolink-headings@7.1.0:
- dependencies:
- '@types/hast': 3.0.4
- '@ungap/structured-clone': 1.3.0
- hast-util-heading-rank: 3.0.0
- hast-util-is-element: 3.0.0
- unified: 11.0.5
- unist-util-visit: 5.0.0
-
- rehype-ignore@2.0.3:
- dependencies:
- hast-util-select: 6.0.4
- unified: 11.0.5
- unist-util-visit: 5.0.0
-
- rehype-parse@9.0.1:
- dependencies:
- '@types/hast': 3.0.4
- hast-util-from-html: 2.0.3
- unified: 11.0.5
-
- rehype-prism-plus@2.0.0:
- dependencies:
- hast-util-to-string: 3.0.1
- parse-numeric-range: 1.3.0
- refractor: 4.9.0
- rehype-parse: 9.0.1
- unist-util-filter: 5.0.1
- unist-util-visit: 5.0.0
-
- rehype-prism-plus@2.0.1:
- dependencies:
- hast-util-to-string: 3.0.1
- parse-numeric-range: 1.3.0
- refractor: 4.9.0
- rehype-parse: 9.0.1
- unist-util-filter: 5.0.1
- unist-util-visit: 5.0.0
-
- rehype-raw@7.0.0:
- dependencies:
- '@types/hast': 3.0.4
- hast-util-raw: 9.1.0
- vfile: 6.0.3
-
- rehype-rewrite@4.0.4:
- dependencies:
- hast-util-select: 6.0.4
- unified: 11.0.5
- unist-util-visit: 5.0.0
-
- rehype-slug@6.0.0:
- dependencies:
- '@types/hast': 3.0.4
- github-slugger: 2.0.0
- hast-util-heading-rank: 3.0.0
- hast-util-to-string: 3.0.1
- unist-util-visit: 5.0.0
-
- rehype-stringify@10.0.1:
- dependencies:
- '@types/hast': 3.0.4
- hast-util-to-html: 9.0.5
- unified: 11.0.5
-
- rehype@13.0.2:
- dependencies:
- '@types/hast': 3.0.4
- rehype-parse: 9.0.1
- rehype-stringify: 10.0.1
- unified: 11.0.5
-
- remark-gfm@4.0.1:
- dependencies:
- '@types/mdast': 4.0.4
- mdast-util-gfm: 3.1.0
- micromark-extension-gfm: 3.0.0
- remark-parse: 11.0.0
- remark-stringify: 11.0.0
- unified: 11.0.5
- transitivePeerDependencies:
- - supports-color
-
- remark-github-blockquote-alert@1.3.1:
- dependencies:
- unist-util-visit: 5.0.0
-
- remark-parse@11.0.0:
- dependencies:
- '@types/mdast': 4.0.4
- mdast-util-from-markdown: 2.0.2
- micromark-util-types: 2.0.2
- unified: 11.0.5
- transitivePeerDependencies:
- - supports-color
-
- remark-rehype@11.1.2:
- dependencies:
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- mdast-util-to-hast: 13.2.1
- unified: 11.0.5
- vfile: 6.0.3
-
- remark-stringify@11.0.0:
- dependencies:
- '@types/mdast': 4.0.4
- mdast-util-to-markdown: 2.1.2
- unified: 11.0.5
-
- require-directory@2.1.1: {}
-
- require-main-filename@2.0.0: {}
-
- reserved-identifiers@1.2.0: {}
-
- resolve-from@4.0.0: {}
-
- resolve-pkg-maps@1.0.0: {}
-
- restore-cursor@5.1.0:
- dependencies:
- onetime: 7.0.0
- signal-exit: 4.1.0
-
- rfdc@1.4.1: {}
-
- rollup@4.55.2:
- dependencies:
- '@types/estree': 1.0.8
- optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.55.2
- '@rollup/rollup-android-arm64': 4.55.2
- '@rollup/rollup-darwin-arm64': 4.55.2
- '@rollup/rollup-darwin-x64': 4.55.2
- '@rollup/rollup-freebsd-arm64': 4.55.2
- '@rollup/rollup-freebsd-x64': 4.55.2
- '@rollup/rollup-linux-arm-gnueabihf': 4.55.2
- '@rollup/rollup-linux-arm-musleabihf': 4.55.2
- '@rollup/rollup-linux-arm64-gnu': 4.55.2
- '@rollup/rollup-linux-arm64-musl': 4.55.2
- '@rollup/rollup-linux-loong64-gnu': 4.55.2
- '@rollup/rollup-linux-loong64-musl': 4.55.2
- '@rollup/rollup-linux-ppc64-gnu': 4.55.2
- '@rollup/rollup-linux-ppc64-musl': 4.55.2
- '@rollup/rollup-linux-riscv64-gnu': 4.55.2
- '@rollup/rollup-linux-riscv64-musl': 4.55.2
- '@rollup/rollup-linux-s390x-gnu': 4.55.2
- '@rollup/rollup-linux-x64-gnu': 4.55.2
- '@rollup/rollup-linux-x64-musl': 4.55.2
- '@rollup/rollup-openbsd-x64': 4.55.2
- '@rollup/rollup-openharmony-arm64': 4.55.2
- '@rollup/rollup-win32-arm64-msvc': 4.55.2
- '@rollup/rollup-win32-ia32-msvc': 4.55.2
- '@rollup/rollup-win32-x64-gnu': 4.55.2
- '@rollup/rollup-win32-x64-msvc': 4.55.2
- fsevents: 2.3.3
-
- scheduler@0.27.0: {}
-
- scslre@0.3.0:
- dependencies:
- '@eslint-community/regexpp': 4.12.2
- refa: 0.12.1
- regexp-ast-analysis: 0.7.1
-
- semver@6.3.1: {}
-
- semver@7.7.3: {}
-
- seroval-plugins@1.4.2(seroval@1.4.2):
- dependencies:
- seroval: 1.4.2
-
- seroval@1.4.2: {}
-
- set-blocking@2.0.0: {}
-
- shebang-command@2.0.0:
- dependencies:
- shebang-regex: 3.0.0
-
- shebang-regex@3.0.0: {}
-
- signal-exit@4.1.0: {}
-
- simple-git-hooks@2.13.1: {}
-
- sisteransi@1.0.5: {}
-
- slice-ansi@7.1.2:
- dependencies:
- ansi-styles: 6.2.3
- is-fullwidth-code-point: 5.1.0
-
- snake-case@3.0.4:
- dependencies:
- dot-case: 3.0.4
- tslib: 2.8.1
-
- sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
- dependencies:
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
-
- source-map-js@1.2.1: {}
-
- source-map@0.6.1: {}
-
- source-map@0.7.6: {}
-
- space-separated-tokens@2.0.2: {}
-
- spdx-exceptions@2.5.0: {}
-
- spdx-expression-parse@4.0.0:
- dependencies:
- spdx-exceptions: 2.5.0
- spdx-license-ids: 3.0.22
-
- spdx-license-ids@3.0.22: {}
-
- string-argv@0.3.2: {}
-
- string-ts@2.3.1: {}
-
- string-width@4.2.3:
- dependencies:
- emoji-regex: 8.0.0
- is-fullwidth-code-point: 3.0.0
- strip-ansi: 6.0.1
-
- string-width@7.2.0:
- dependencies:
- emoji-regex: 10.6.0
- get-east-asian-width: 1.4.0
- strip-ansi: 7.1.2
-
- string-width@8.1.0:
- dependencies:
- get-east-asian-width: 1.4.0
- strip-ansi: 7.1.2
-
- stringify-entities@4.0.4:
- dependencies:
- character-entities-html4: 2.1.0
- character-entities-legacy: 3.0.0
-
- strip-ansi@6.0.1:
- dependencies:
- ansi-regex: 5.0.1
-
- strip-ansi@7.1.2:
- dependencies:
- ansi-regex: 6.2.2
-
- strip-indent@4.1.1: {}
-
- strip-json-comments@3.1.1: {}
-
- style-to-js@1.1.21:
- dependencies:
- style-to-object: 1.0.14
-
- style-to-object@1.0.14:
- dependencies:
- inline-style-parser: 0.2.7
-
- supports-color@7.2.0:
- dependencies:
- has-flag: 4.0.0
-
- svg-parser@2.0.4: {}
-
- synckit@0.11.12:
- dependencies:
- '@pkgr/core': 0.2.9
-
- tagged-tag@1.0.0: {}
-
- tailwind-merge@3.4.0: {}
-
- tailwindcss@4.1.18: {}
-
- tapable@2.3.0: {}
-
- tiny-invariant@1.3.3: {}
-
- tiny-warning@1.0.3: {}
-
- tinyexec@1.0.2: {}
-
- tinyglobby@0.2.15:
- dependencies:
- fdir: 6.5.0(picomatch@4.0.3)
- picomatch: 4.0.3
-
- to-regex-range@5.0.1:
- dependencies:
- is-number: 7.0.0
-
- to-valid-identifier@1.0.0:
- dependencies:
- '@sindresorhus/base62': 1.0.0
- reserved-identifiers: 1.2.0
-
- toml-eslint-parser@0.10.1:
- dependencies:
- eslint-visitor-keys: 3.4.3
-
- trim-lines@3.0.1: {}
-
- trough@2.2.0: {}
-
- ts-api-utils@2.4.0(typescript@5.9.3):
- dependencies:
- typescript: 5.9.3
-
- ts-declaration-location@1.0.7(typescript@5.9.3):
- dependencies:
- picomatch: 4.0.3
- typescript: 5.9.3
-
- ts-pattern@5.9.0: {}
-
- tslib@2.8.1: {}
-
- tsx@4.21.0:
- dependencies:
- esbuild: 0.27.2
- get-tsconfig: 4.13.0
- optionalDependencies:
- fsevents: 2.3.3
-
- tw-animate-css@1.4.0: {}
-
- type-check@0.4.0:
- dependencies:
- prelude-ls: 1.2.1
-
- type-fest@5.4.1:
- dependencies:
- tagged-tag: 1.0.0
-
- typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- typescript: 5.9.3
- transitivePeerDependencies:
- - supports-color
-
- typescript@5.9.3: {}
-
- ufo@1.6.3: {}
-
- undici-types@7.16.0: {}
-
- unified@11.0.5:
- dependencies:
- '@types/unist': 3.0.3
- bail: 2.0.2
- devlop: 1.1.0
- extend: 3.0.2
- is-plain-obj: 4.1.0
- trough: 2.2.0
- vfile: 6.0.3
-
- unist-util-filter@5.0.1:
- dependencies:
- '@types/unist': 3.0.3
- unist-util-is: 6.0.1
- unist-util-visit-parents: 6.0.2
-
- unist-util-is@6.0.1:
- dependencies:
- '@types/unist': 3.0.3
-
- unist-util-position@5.0.0:
- dependencies:
- '@types/unist': 3.0.3
-
- unist-util-stringify-position@4.0.0:
- dependencies:
- '@types/unist': 3.0.3
-
- unist-util-visit-parents@6.0.2:
- dependencies:
- '@types/unist': 3.0.3
- unist-util-is: 6.0.1
-
- unist-util-visit@5.0.0:
- dependencies:
- '@types/unist': 3.0.3
- unist-util-is: 6.0.1
- unist-util-visit-parents: 6.0.2
-
- unplugin@2.3.11:
- dependencies:
- '@jridgewell/remapping': 2.3.5
- acorn: 8.15.0
- picomatch: 4.0.3
- webpack-virtual-modules: 0.6.2
-
- update-browserslist-db@1.2.3(browserslist@4.28.1):
- dependencies:
- browserslist: 4.28.1
- escalade: 3.2.0
- picocolors: 1.1.1
-
- uri-js@4.4.1:
- dependencies:
- punycode: 2.3.1
-
- use-callback-ref@1.3.3(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- react: 19.2.3
- tslib: 2.8.1
- optionalDependencies:
- '@types/react': 19.2.8
-
- use-sidecar@1.1.3(@types/react@19.2.8)(react@19.2.3):
- dependencies:
- detect-node-es: 1.1.0
- react: 19.2.3
- tslib: 2.8.1
- optionalDependencies:
- '@types/react': 19.2.8
-
- use-sync-external-store@1.6.0(react@19.2.3):
- dependencies:
- react: 19.2.3
-
- utf8@3.0.0: {}
-
- util-deprecate@1.0.2: {}
-
- vaul@1.1.2(@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/react-dialog': 1.1.15(@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)
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
- transitivePeerDependencies:
- - '@types/react'
- - '@types/react-dom'
-
- vfile-location@5.0.3:
- dependencies:
- '@types/unist': 3.0.3
- vfile: 6.0.3
-
- vfile-message@4.0.3:
- dependencies:
- '@types/unist': 3.0.3
- unist-util-stringify-position: 4.0.0
-
- vfile@6.0.3:
- dependencies:
- '@types/unist': 3.0.3
- vfile-message: 4.0.3
-
- victory-vendor@36.9.2:
- dependencies:
- '@types/d3-array': 3.2.2
- '@types/d3-ease': 3.0.2
- '@types/d3-interpolate': 3.0.4
- '@types/d3-scale': 4.0.9
- '@types/d3-shape': 3.1.8
- '@types/d3-time': 3.0.4
- '@types/d3-timer': 3.0.2
- d3-array: 3.2.4
- d3-ease: 3.0.1
- d3-interpolate: 3.0.1
- d3-scale: 4.0.2
- d3-shape: 3.2.0
- d3-time: 3.1.0
- d3-timer: 3.0.1
-
- vite-plugin-svgr@4.5.0(rollup@4.55.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)):
- dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.55.2)
- '@svgr/core': 8.1.0(typescript@5.9.3)
- '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
- vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
- transitivePeerDependencies:
- - rollup
- - supports-color
- - typescript
-
- vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2):
- dependencies:
- esbuild: 0.27.2
- fdir: 6.5.0(picomatch@4.0.3)
- picomatch: 4.0.3
- postcss: 8.5.6
- rollup: 4.55.2
- tinyglobby: 0.2.15
- optionalDependencies:
- '@types/node': 25.0.9
- fsevents: 2.3.3
- jiti: 2.6.1
- lightningcss: 1.30.2
- tsx: 4.21.0
- yaml: 2.8.2
-
- vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- debug: 4.4.3
- eslint: 9.39.2(jiti@2.6.1)
- eslint-scope: 8.4.0
- eslint-visitor-keys: 4.2.1
- espree: 10.4.0
- esquery: 1.7.0
- semver: 7.7.3
- transitivePeerDependencies:
- - supports-color
-
- web-namespaces@2.0.1: {}
-
- webpack-virtual-modules@0.6.2: {}
-
- which-module@2.0.1: {}
-
- which@2.0.2:
- dependencies:
- isexe: 2.0.0
-
- word-wrap@1.2.5: {}
-
- wrap-ansi@6.2.0:
- dependencies:
- ansi-styles: 4.3.0
- string-width: 4.2.3
- strip-ansi: 6.0.1
-
- wrap-ansi@9.0.2:
- dependencies:
- ansi-styles: 6.2.3
- string-width: 7.2.0
- strip-ansi: 7.1.2
-
- xml-name-validator@4.0.0: {}
-
- y18n@4.0.3: {}
-
- yallist@3.1.1: {}
-
- yaml-eslint-parser@1.3.2:
- dependencies:
- eslint-visitor-keys: 3.4.3
- yaml: 2.8.2
-
- yaml-eslint-parser@2.0.0:
- dependencies:
- eslint-visitor-keys: 5.0.0
- yaml: 2.8.2
-
- yaml@2.8.2: {}
-
- yargs-parser@18.1.3:
- dependencies:
- camelcase: 5.3.1
- decamelize: 1.2.0
-
- yargs@15.4.1:
- dependencies:
- cliui: 6.0.0
- decamelize: 1.2.0
- find-up: 4.1.0
- get-caller-file: 2.0.5
- require-directory: 2.1.1
- require-main-filename: 2.0.0
- set-blocking: 2.0.0
- string-width: 4.2.3
- which-module: 2.0.1
- y18n: 4.0.3
- yargs-parser: 18.1.3
-
- yocto-queue@0.1.0: {}
-
- zod-validation-error@4.0.2(zod@4.3.5):
- dependencies:
- zod: 4.3.5
-
- zod@3.25.76: {}
-
- zod@4.3.5: {}
-
- zustand@5.0.10(@types/react@19.2.8)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)):
- optionalDependencies:
- '@types/react': 19.2.8
- immer: 11.1.3
- react: 19.2.3
- use-sync-external-store: 1.6.0(react@19.2.3)
-
- zwitch@2.0.4: {}
diff --git a/client/cms/src/App.tsx b/client/cms/src/App.tsx
deleted file mode 100644
index 71c1efb..0000000
--- a/client/cms/src/App.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ThemeProvider } from '@/components/theme-provider';
-
-function App() {
- return (
-
- Hello world
-
- );
-}
-
-export { App };
diff --git a/client/cms/src/assets/nixos.svg b/client/cms/src/assets/nixos.svg
deleted file mode 100644
index 92f670b..0000000
--- a/client/cms/src/assets/nixos.svg
+++ /dev/null
@@ -1,28 +0,0 @@
-
diff --git a/client/cms/src/components/checkin/qr-dialog.tsx b/client/cms/src/components/checkin/qr-dialog.tsx
deleted file mode 100644
index f4bb318..0000000
--- a/client/cms/src/components/checkin/qr-dialog.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { useState } from 'react';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- 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(
- { eventId }: { eventId: string },
-) {
- const [open, setOpen] = useState(false);
- return (
-
- );
-}
-
-function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) {
- const { data } = useCheckinCode(eventId, enabled);
- return data
- ? (
- <>
-
-
-
-
-
- {data.data.checkin_code}
-
-
- >
- )
- : (
-
- );
-}
-
-function QrSectionSkeleton() {
- return (
- <>
-
-
-
-
-
- Loading...
-
-
- >
- );
-}
diff --git a/client/cms/src/components/hoc/with-fallback.tsx b/client/cms/src/components/hoc/with-fallback.tsx
deleted file mode 100644
index 5d4c0e6..0000000
--- a/client/cms/src/components/hoc/with-fallback.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { ReactNode } from 'react';
-import React, { Suspense } from 'react';
-
-export function withFallback(
- Component: React.ComponentType
,
- fallback: ReactNode,
-) {
- const Wrapped: React.FC
= (props) => {
- return (
-
-
-
- );
- };
-
- Wrapped.displayName = `withFallback(${Component.displayName! || Component.name || 'Component'
- })`;
-
- return Wrapped;
-}
diff --git a/client/cms/src/components/login-form.tsx b/client/cms/src/components/login-form.tsx
deleted file mode 100644
index 8ef955b..0000000
--- a/client/cms/src/components/login-form.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import type { TurnstileInstance } from '@marsidev/react-turnstile';
-import type { AuthorizeSearchParams } from '@/routes/authorize';
-import { Turnstile } from '@marsidev/react-turnstile';
-import { useNavigate } from '@tanstack/react-router';
-import { useRef, useState } from 'react';
-import { toast } from 'sonner';
-import NixOSLogo from '@/assets/nixos.svg?react';
-import { Button } from '@/components/ui/button';
-import {
- Field,
- FieldGroup,
- FieldLabel,
-} from '@/components/ui/field';
-import { Input } from '@/components/ui/input';
-import { useGetMagicLink } from '@/hooks/data/useGetMagicLink';
-import { cn } from '@/lib/utils';
-
-export function LoginForm({
- oauthParams,
- className,
- ...props
-}: React.ComponentProps<'div'> & {
- oauthParams: AuthorizeSearchParams;
-}) {
- const formRef = useRef(null);
- const turnstileRef = useRef(null);
- const [token, setToken] = useState(null);
- const { mutateAsync, isPending } = useGetMagicLink();
- const navigate = useNavigate();
-
- const handleSubmit = (event: React.FormEvent) => {
- event.preventDefault();
- const formData = new FormData(formRef.current!);
- const email = formData.get('email')! as string;
- mutateAsync({ email, turnstile_token: token!, ...oauthParams }).then(() => {
- void navigate({ to: '/magicLinkSent', search: { email } });
- }).catch((error) => {
- console.error(error);
- toast.error('请求登录链接失败');
- turnstileRef.current?.reset();
- });
- };
-
- return (
-
-
-
{
- setToken(token);
- }}
- />
-
- );
-}
diff --git a/client/cms/src/components/profile/edit-profile-dialog.tsx b/client/cms/src/components/profile/edit-profile-dialog.tsx
deleted file mode 100644
index e9efd34..0000000
--- a/client/cms/src/components/profile/edit-profile-dialog.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import { useForm } from '@tanstack/react-form';
-import { toast } from 'sonner';
-import z from 'zod';
-import { Button } from '@/components/ui/button';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@/components/ui/dialog';
-import {
- Field,
- FieldError,
- FieldLabel,
-} from '@/components/ui/field';
-import {
- Input,
-} from '@/components/ui/input';
-import { useUpdateUser } from '@/hooks/data/useUpdateUser';
-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),
-});
-export function EditProfileDialog() {
- const { data: user } = useUserInfo();
- const { mutateAsync } = useUpdateUser();
-
- const form = useForm({
- defaultValues: {
- avatar: user.avatar,
- username: user.username,
- nickname: user.nickname,
- subtitle: user.subtitle,
- },
- validators: {
- onBlur: formSchema,
- },
- onSubmit: async ({
- value,
- }) => {
- try {
- await mutateAsync(value);
- toast.success('个人资料更新成功');
- }
- catch (error) {
- console.error('Form submission error', error);
- toast.error('更新个人资料失败,请重试');
- }
- },
- });
-
- return (
-
- );
-}
diff --git a/client/cms/src/components/profile/main-profile.tsx b/client/cms/src/components/profile/main-profile.tsx
deleted file mode 100644
index 573f3e5..0000000
--- a/client/cms/src/components/profile/main-profile.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import MDEditor from '@uiw/react-md-editor';
-import { isNil } from 'lodash-es';
-import { Mail, Pencil } from 'lucide-react';
-import { useState } from 'react';
-import Markdown from 'react-markdown';
-import { toast } from 'sonner';
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { useUpdateUser } from '@/hooks/data/useUpdateUser';
-import { useUserInfo } from '@/hooks/data/useUserInfo';
-import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
-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 [enableBioEdit, setEnableBioEdit] = useState(false);
- const { mutateAsync } = useUpdateUser();
-
- return (
-
-
-
-
-
-
-
- CN
-
-
- {user.nickname}
- {user.subtitle}
-
-
-
-
- {user.email}
-
-
-
-
-
-
- {/* Bio */}
- {enableBioEdit
- ? (
-
- )
- : {bio}
}
-
-
-
- );
-}
diff --git a/client/cms/src/components/sidebar/app-sidebar.tsx b/client/cms/src/components/sidebar/app-sidebar.tsx
deleted file mode 100644
index 0d1a945..0000000
--- a/client/cms/src/components/sidebar/app-sidebar.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as React from 'react';
-import NixOSLogo from '@/assets/nixos.svg?react';
-import { NavMain } from '@/components/sidebar/nav-main';
-import { NavSecondary } from '@/components/sidebar/nav-secondary';
-import {
- Sidebar,
- SidebarContent,
- SidebarFooter,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
-} from '@/components/ui/sidebar';
-import { navData } from '@/lib/navData';
-import { NavUser } from './nav-user';
-
-export function AppSidebar({ ...props }: React.ComponentProps) {
- return (
-
-
-
-
-
-
-
- Nix CN CMS
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/client/cms/src/components/sidebar/nav-main.tsx b/client/cms/src/components/sidebar/nav-main.tsx
deleted file mode 100644
index fc7c13b..0000000
--- a/client/cms/src/components/sidebar/nav-main.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { Icon } from '@tabler/icons-react';
-
-import { Link } from '@tanstack/react-router';
-import {
- SidebarGroup,
- SidebarGroupContent,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
-} from '@/components/ui/sidebar';
-
-export function NavMain({
- items,
-}: {
- items: {
- title: string;
- url: string;
- icon?: Icon;
- }[];
-}) {
- return (
-
-
-
- {items.map(item => (
-
-
- {({ isActive }) => {
- return (
-
- {item.icon && }
- {item.title}
-
- );
- }}
-
-
- ))}
-
-
-
- );
-}
diff --git a/client/cms/src/components/sidebar/nav-secondary.tsx b/client/cms/src/components/sidebar/nav-secondary.tsx
deleted file mode 100644
index b0da89c..0000000
--- a/client/cms/src/components/sidebar/nav-secondary.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-
-import type { Icon } from '@tabler/icons-react';
-import { Link } from '@tanstack/react-router';
-
-import * as React from 'react';
-import {
- SidebarGroup,
- SidebarGroupContent,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
-} from '@/components/ui/sidebar';
-
-export function NavSecondary({
- items,
- ...props
-}: {
- items: {
- title: string;
- url: string;
- icon: Icon;
- }[];
-} & React.ComponentPropsWithoutRef) {
- return (
-
-
-
- {items.map(item => (
-
-
- {({ isActive }) => {
- return (
-
-
- {item.title}
-
- );
- }}
-
-
- ))}
-
-
-
- );
-}
diff --git a/client/cms/src/components/sidebar/nav-user.tsx b/client/cms/src/components/sidebar/nav-user.tsx
deleted file mode 100644
index 8356344..0000000
--- a/client/cms/src/components/sidebar/nav-user.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
- IconDotsVertical,
- IconLogout,
-} from '@tabler/icons-react';
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from '@/components/ui/avatar';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import {
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- useSidebar,
-} from '@/components/ui/sidebar';
-import { useUserInfo } from '@/hooks/data/useUserInfo';
-import { useLogout } from '@/hooks/useLogout';
-import { withFallback } from '../hoc/with-fallback';
-import { Skeleton } from '../ui/skeleton';
-
-function NavUser_() {
- const { isMobile } = useSidebar();
- const { data: user } = useUserInfo();
- const { logout } = useLogout();
-
- return (
-
-
-
-
-
-
-
- CN
-
-
- {user.nickname}
-
- {user.email}
-
-
-
-
-
-
-
-
-
-
- CN
-
-
- {user.nickname}
-
- {user.email}
-
-
-
-
-
-
-
- 登出
-
-
-
-
-
- );
-}
-
-function NavUserSkeleton() {
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-export const NavUser = withFallback(NavUser_, );
diff --git a/client/cms/src/components/site-header.tsx b/client/cms/src/components/site-header.tsx
deleted file mode 100644
index 9ecc44b..0000000
--- a/client/cms/src/components/site-header.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useRouterState } from '@tanstack/react-router';
-import { Separator } from '@/components/ui/separator';
-import { SidebarTrigger } from '@/components/ui/sidebar';
-import { navData } from '@/lib/navData';
-
-export function SiteHeader() {
- const pathname = useRouterState({ select: state => state.location.pathname });
- const allNavItems = [...navData.navMain, ...navData.navSecondary];
- const currentTitle
- = allNavItems.find(item =>
- item.url === '/'
- ? pathname === '/'
- : pathname.startsWith(item.url),
- )?.title ?? '工作台';
-
- return (
-
-
-
-
-
{currentTitle}
-
-
- );
-}
diff --git a/client/cms/src/components/theme-provider.tsx b/client/cms/src/components/theme-provider.tsx
deleted file mode 100644
index 14bc663..0000000
--- a/client/cms/src/components/theme-provider.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { Theme } from '@/hooks/useTheme';
-import { useEffect, useState } from 'react';
-import { ThemeProviderContext } from '@/hooks/useTheme';
-
-interface ThemeProviderProps {
- children: React.ReactNode;
- defaultTheme?: Theme;
- storageKey?: string;
-}
-
-export function ThemeProvider({
- children,
- defaultTheme = 'dark',
- storageKey = 'vite-ui-theme',
- ...props
-}: ThemeProviderProps) {
- const [theme, setTheme] = useState(
- () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
- );
-
- useEffect(() => {
- const root = window.document.documentElement;
-
- root.classList.remove('light', 'dark');
-
- if (theme === 'system') {
- const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
- .matches
- ? 'dark'
- : 'light';
-
- root.classList.add(systemTheme);
- return;
- }
-
- root.classList.add(theme);
- }, [theme]);
-
- // eslint-disable-next-line react/no-unstable-context-value
- const value = {
- theme,
- setTheme: (theme: Theme) => {
- localStorage.setItem(storageKey, theme);
- setTheme(theme);
- },
- };
-
- return (
-
- {children}
-
- );
-}
diff --git a/client/cms/src/components/ui/avatar.tsx b/client/cms/src/components/ui/avatar.tsx
deleted file mode 100644
index 71e428b..0000000
--- a/client/cms/src/components/ui/avatar.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import { cn } from "@/lib/utils"
-
-function Avatar({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AvatarImage({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AvatarFallback({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/cms/src/components/ui/badge.tsx b/client/cms/src/components/ui/badge.tsx
deleted file mode 100644
index fd3a406..0000000
--- a/client/cms/src/components/ui/badge.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const badgeVariants = cva(
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
- destructive:
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span"
-
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/client/cms/src/components/ui/breadcrumb.tsx b/client/cms/src/components/ui/breadcrumb.tsx
deleted file mode 100644
index eb88f32..0000000
--- a/client/cms/src/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { ChevronRight, MoreHorizontal } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
- return
-}
-
-function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
- return (
-
- )
-}
-
-function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
- return (
-
- )
-}
-
-function BreadcrumbLink({
- asChild,
- className,
- ...props
-}: React.ComponentProps<"a"> & {
- asChild?: boolean
-}) {
- const Comp = asChild ? Slot : "a"
-
- return (
-
- )
-}
-
-function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
- return (
-
- )
-}
-
-function BreadcrumbSeparator({
- children,
- className,
- ...props
-}: React.ComponentProps<"li">) {
- return (
- svg]:size-3.5", className)}
- {...props}
- >
- {children ?? }
-
- )
-}
-
-function BreadcrumbEllipsis({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
-
- More
-
- )
-}
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
diff --git a/client/cms/src/components/ui/button.tsx b/client/cms/src/components/ui/button.tsx
deleted file mode 100644
index e1de1eb..0000000
--- a/client/cms/src/components/ui/button.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { VariantProps } from 'class-variance-authority';
-import { Slot } from '@radix-ui/react-slot';
-import { cva } from 'class-variance-authority';
-import * as React from 'react';
-
-import { cn } from '@/lib/utils';
-
-const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive:
- 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
- outline:
- 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
- secondary:
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost:
- 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
- link: 'text-primary underline-offset-4 hover:underline',
- },
- size: {
- 'default': 'h-9 px-4 py-2 has-[>svg]:px-3',
- 'sm': 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
- 'lg': 'h-10 rounded-md px-6 has-[>svg]:px-4',
- 'icon': 'size-9',
- 'icon-sm': 'size-8',
- 'icon-lg': 'size-10',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- },
-);
-
-function Button({
- className,
- variant = 'default',
- size = 'default',
- asChild = false,
- ...props
-}: React.ComponentProps<'button'>
- & VariantProps & {
- asChild?: boolean;
- }) {
- const Comp = asChild ? Slot : 'button';
-
- return (
-
- );
-}
-
-export { Button, buttonVariants };
diff --git a/client/cms/src/components/ui/card.tsx b/client/cms/src/components/ui/card.tsx
deleted file mode 100644
index 681ad98..0000000
--- a/client/cms/src/components/ui/card.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-
-function Card({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
diff --git a/client/cms/src/components/ui/chart.tsx b/client/cms/src/components/ui/chart.tsx
deleted file mode 100644
index 48d2724..0000000
--- a/client/cms/src/components/ui/chart.tsx
+++ /dev/null
@@ -1,355 +0,0 @@
-import * as React from "react"
-import * as RechartsPrimitive from "recharts"
-
-import { cn } from "@/lib/utils"
-
-// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const
-
-export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode
- icon?: React.ComponentType
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record }
- )
-}
-
-type ChartContextProps = {
- config: ChartConfig
-}
-
-const ChartContext = React.createContext(null)
-
-function useChart() {
- const context = React.useContext(ChartContext)
-
- if (!context) {
- throw new Error("useChart must be used within a ")
- }
-
- return context
-}
-
-function ChartContainer({
- id,
- className,
- children,
- config,
- ...props
-}: React.ComponentProps<"div"> & {
- config: ChartConfig
- children: React.ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"]
-}) {
- const uniqueId = React.useId()
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
-
- return (
-
-
-
-
- {children}
-
-
-
- )
-}
-
-const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([, config]) => config.theme || config.color
- )
-
- if (!colorConfig.length) {
- return null
- }
-
- return (
-
-
-
-
diff --git a/client/mobile/android/app/src/main/res/values/styles.xml b/client/mobile/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index cb1ef88..0000000
--- a/client/mobile/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/client/mobile/android/app/src/profile/AndroidManifest.xml b/client/mobile/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index 399f698..0000000
--- a/client/mobile/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/client/mobile/android/build.gradle.kts b/client/mobile/android/build.gradle.kts
deleted file mode 100644
index dbee657..0000000
--- a/client/mobile/android/build.gradle.kts
+++ /dev/null
@@ -1,24 +0,0 @@
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-val newBuildDir: Directory =
- rootProject.layout.buildDirectory
- .dir("../../build")
- .get()
-rootProject.layout.buildDirectory.value(newBuildDir)
-
-subprojects {
- val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
- project.layout.buildDirectory.value(newSubprojectBuildDir)
-}
-subprojects {
- project.evaluationDependsOn(":app")
-}
-
-tasks.register("clean") {
- delete(rootProject.layout.buildDirectory)
-}
diff --git a/client/mobile/android/gradle.properties b/client/mobile/android/gradle.properties
deleted file mode 100644
index fbee1d8..0000000
--- a/client/mobile/android/gradle.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
-android.useAndroidX=true
diff --git a/client/mobile/android/gradle/wrapper/gradle-wrapper.properties b/client/mobile/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index e4ef43f..0000000
--- a/client/mobile/android/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
diff --git a/client/mobile/android/settings.gradle.kts b/client/mobile/android/settings.gradle.kts
deleted file mode 100644
index ca7fe06..0000000
--- a/client/mobile/android/settings.gradle.kts
+++ /dev/null
@@ -1,26 +0,0 @@
-pluginManagement {
- val flutterSdkPath =
- run {
- val properties = java.util.Properties()
- file("local.properties").inputStream().use { properties.load(it) }
- val flutterSdkPath = properties.getProperty("flutter.sdk")
- require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
- flutterSdkPath
- }
-
- includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-}
-
-plugins {
- id("dev.flutter.flutter-plugin-loader") version "1.0.0"
- id("com.android.application") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.20" apply false
-}
-
-include(":app")
diff --git a/client/mobile/ios/.gitignore b/client/mobile/ios/.gitignore
deleted file mode 100644
index 7a7f987..0000000
--- a/client/mobile/ios/.gitignore
+++ /dev/null
@@ -1,34 +0,0 @@
-**/dgph
-*.mode1v3
-*.mode2v3
-*.moved-aside
-*.pbxuser
-*.perspectivev3
-**/*sync/
-.sconsign.dblite
-.tags*
-**/.vagrant/
-**/DerivedData/
-Icon?
-**/Pods/
-**/.symlinks/
-profile
-xcuserdata
-**/.generated/
-Flutter/App.framework
-Flutter/Flutter.framework
-Flutter/Flutter.podspec
-Flutter/Generated.xcconfig
-Flutter/ephemeral/
-Flutter/app.flx
-Flutter/app.zip
-Flutter/flutter_assets/
-Flutter/flutter_export_environment.sh
-ServiceDefinitions.json
-Runner/GeneratedPluginRegistrant.*
-
-# Exceptions to above rules.
-!default.mode1v3
-!default.mode2v3
-!default.pbxuser
-!default.perspectivev3
diff --git a/client/mobile/ios/Flutter/AppFrameworkInfo.plist b/client/mobile/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100644
index 1dc6cf7..0000000
--- a/client/mobile/ios/Flutter/AppFrameworkInfo.plist
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- App
- CFBundleIdentifier
- io.flutter.flutter.app
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- App
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleSignature
- ????
- CFBundleVersion
- 1.0
- MinimumOSVersion
- 13.0
-
-
diff --git a/client/mobile/ios/Flutter/Debug.xcconfig b/client/mobile/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/client/mobile/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/client/mobile/ios/Flutter/Release.xcconfig b/client/mobile/ios/Flutter/Release.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/client/mobile/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/client/mobile/ios/Runner.xcodeproj/project.pbxproj b/client/mobile/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index 5a8df3b..0000000
--- a/client/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,616 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 54;
- objects = {
-
-/* Begin PBXBuildFile section */
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 97C146E61CF9000F007C117D /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 97C146ED1CF9000F007C117D;
- remoteInfo = Runner;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
- 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
- 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
- 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 97C146EB1CF9000F007C117D /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 331C8082294A63A400263BE5 /* RunnerTests */ = {
- isa = PBXGroup;
- children = (
- 331C807B294A618700263BE5 /* RunnerTests.swift */,
- );
- path = RunnerTests;
- sourceTree = "";
- };
- 9740EEB11CF90186004384FC /* Flutter */ = {
- isa = PBXGroup;
- children = (
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
- 9740EEB21CF90195004384FC /* Debug.xcconfig */,
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
- 9740EEB31CF90195004384FC /* Generated.xcconfig */,
- );
- name = Flutter;
- sourceTree = "";
- };
- 97C146E51CF9000F007C117D = {
- isa = PBXGroup;
- children = (
- 9740EEB11CF90186004384FC /* Flutter */,
- 97C146F01CF9000F007C117D /* Runner */,
- 97C146EF1CF9000F007C117D /* Products */,
- 331C8082294A63A400263BE5 /* RunnerTests */,
- );
- sourceTree = "";
- };
- 97C146EF1CF9000F007C117D /* Products */ = {
- isa = PBXGroup;
- children = (
- 97C146EE1CF9000F007C117D /* Runner.app */,
- 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
- );
- name = Products;
- sourceTree = "";
- };
- 97C146F01CF9000F007C117D /* Runner */ = {
- isa = PBXGroup;
- children = (
- 97C146FA1CF9000F007C117D /* Main.storyboard */,
- 97C146FD1CF9000F007C117D /* Assets.xcassets */,
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
- 97C147021CF9000F007C117D /* Info.plist */,
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
- );
- path = Runner;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 331C8080294A63A400263BE5 /* RunnerTests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
- buildPhases = (
- 331C807D294A63A400263BE5 /* Sources */,
- 331C807F294A63A400263BE5 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 331C8086294A63A400263BE5 /* PBXTargetDependency */,
- );
- name = RunnerTests;
- productName = RunnerTests;
- productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
- 97C146ED1CF9000F007C117D /* Runner */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
- buildPhases = (
- 9740EEB61CF901F6004384FC /* Run Script */,
- 97C146EA1CF9000F007C117D /* Sources */,
- 97C146EB1CF9000F007C117D /* Frameworks */,
- 97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = Runner;
- productName = Runner;
- productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 97C146E61CF9000F007C117D /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = YES;
- LastUpgradeCheck = 1510;
- ORGANIZATIONNAME = "";
- TargetAttributes = {
- 331C8080294A63A400263BE5 = {
- CreatedOnToolsVersion = 14.0;
- TestTargetID = 97C146ED1CF9000F007C117D;
- };
- 97C146ED1CF9000F007C117D = {
- CreatedOnToolsVersion = 7.3.1;
- LastSwiftMigration = 1100;
- };
- };
- };
- buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 97C146E51CF9000F007C117D;
- productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 97C146ED1CF9000F007C117D /* Runner */,
- 331C8080294A63A400263BE5 /* RunnerTests */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 331C807F294A63A400263BE5 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 97C146EC1CF9000F007C117D /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
- );
- name = "Thin Binary";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
- };
- 9740EEB61CF901F6004384FC /* Run Script */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- name = "Run Script";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 331C807D294A63A400263BE5 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 97C146EA1CF9000F007C117D /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 97C146ED1CF9000F007C117D /* Runner */;
- targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin PBXVariantGroup section */
- 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C146FB1CF9000F007C117D /* Base */,
- );
- name = Main.storyboard;
- sourceTree = "";
- };
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C147001CF9000F007C117D /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- 249021D3217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Profile;
- };
- 249021D4217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = io.asnk.applications.nixcn;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Profile;
- };
- 331C8088294A63A400263BE5 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = io.asnk.applications.nixcn.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Debug;
- };
- 331C8089294A63A400263BE5 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = io.asnk.applications.nixcn.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Release;
- };
- 331C808A294A63A400263BE5 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = io.asnk.applications.nixcn.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Profile;
- };
- 97C147031CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 97C147041CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 97C147061CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = io.asnk.applications.nixcn;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Debug;
- };
- 97C147071CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = io.asnk.applications.nixcn;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 331C8088294A63A400263BE5 /* Debug */,
- 331C8089294A63A400263BE5 /* Release */,
- 331C808A294A63A400263BE5 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147031CF9000F007C117D /* Debug */,
- 97C147041CF9000F007C117D /* Release */,
- 249021D3217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147061CF9000F007C117D /* Debug */,
- 97C147071CF9000F007C117D /* Release */,
- 249021D4217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
diff --git a/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/client/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/client/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/client/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index e3773d4..0000000
--- a/client/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/client/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/client/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/client/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/client/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/client/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/client/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/client/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/client/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/client/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/client/mobile/ios/Runner/AppDelegate.swift b/client/mobile/ios/Runner/AppDelegate.swift
deleted file mode 100644
index 6266644..0000000
--- a/client/mobile/ios/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import Flutter
-import UIKit
-
-@main
-@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-}
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d36b1fa..0000000
--- a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 7353c41..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index 6ed2d93..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cd7b00..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index fe73094..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index 321773c..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index 502f463..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index e9f5fea..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index 84ac32a..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 8953cba..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index 0467bf1..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100644
index 0bedcf2..0000000
--- a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "LaunchImage.png",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ
diff --git a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100644
index 89c2725..0000000
--- a/client/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Launch Screen Assets
-
-You can customize the launch screen with your own desired assets by replacing the image files in this directory.
-
-You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/client/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/client/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f2e259c..0000000
--- a/client/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/client/mobile/ios/Runner/Base.lproj/Main.storyboard b/client/mobile/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100644
index f3c2851..0000000
--- a/client/mobile/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/client/mobile/ios/Runner/Info.plist b/client/mobile/ios/Runner/Info.plist
deleted file mode 100644
index 79b7574..0000000
--- a/client/mobile/ios/Runner/Info.plist
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Nixcn
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- nixcn
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
-
-
diff --git a/client/mobile/ios/Runner/Runner-Bridging-Header.h b/client/mobile/ios/Runner/Runner-Bridging-Header.h
deleted file mode 100644
index 308a2a5..0000000
--- a/client/mobile/ios/Runner/Runner-Bridging-Header.h
+++ /dev/null
@@ -1 +0,0 @@
-#import "GeneratedPluginRegistrant.h"
diff --git a/client/mobile/ios/RunnerTests/RunnerTests.swift b/client/mobile/ios/RunnerTests/RunnerTests.swift
deleted file mode 100644
index 86a7c3b..0000000
--- a/client/mobile/ios/RunnerTests/RunnerTests.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-import Flutter
-import UIKit
-import XCTest
-
-class RunnerTests: XCTestCase {
-
- func testExample() {
- // If you add code to the Runner application, consider adding tests here.
- // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
- }
-
-}
diff --git a/client/mobile/lib/main.dart b/client/mobile/lib/main.dart
deleted file mode 100644
index 244a702..0000000
--- a/client/mobile/lib/main.dart
+++ /dev/null
@@ -1,122 +0,0 @@
-import 'package:flutter/material.dart';
-
-void main() {
- runApp(const MyApp());
-}
-
-class MyApp extends StatelessWidget {
- const MyApp({super.key});
-
- // This widget is the root of your application.
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- // This is the theme of your application.
- //
- // TRY THIS: Try running your application with "flutter run". You'll see
- // the application has a purple toolbar. Then, without quitting the app,
- // try changing the seedColor in the colorScheme below to Colors.green
- // and then invoke "hot reload" (save your changes or press the "hot
- // reload" button in a Flutter-supported IDE, or press "r" if you used
- // the command line to start the app).
- //
- // Notice that the counter didn't reset back to zero; the application
- // state is not lost during the reload. To reset the state, use hot
- // restart instead.
- //
- // This works for code too, not just values: Most code changes can be
- // tested with just a hot reload.
- colorScheme: .fromSeed(seedColor: Colors.deepPurple),
- ),
- home: const MyHomePage(title: 'Flutter Demo Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- const MyHomePage({super.key, required this.title});
-
- // This widget is the home page of your application. It is stateful, meaning
- // that it has a State object (defined below) that contains fields that affect
- // how it looks.
-
- // This class is the configuration for the state. It holds the values (in this
- // case the title) provided by the parent (in this case the App widget) and
- // used by the build method of the State. Fields in a Widget subclass are
- // always marked "final".
-
- final String title;
-
- @override
- State createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State {
- int _counter = 0;
-
- void _incrementCounter() {
- setState(() {
- // This call to setState tells the Flutter framework that something has
- // changed in this State, which causes it to rerun the build method below
- // so that the display can reflect the updated values. If we changed
- // _counter without calling setState(), then the build method would not be
- // called again, and so nothing would appear to happen.
- _counter++;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- // This method is rerun every time setState is called, for instance as done
- // by the _incrementCounter method above.
- //
- // The Flutter framework has been optimized to make rerunning build methods
- // fast, so that you can just rebuild anything that needs updating rather
- // than having to individually change instances of widgets.
- return Scaffold(
- appBar: AppBar(
- // TRY THIS: Try changing the color here to a specific color (to
- // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
- // change color while the other colors stay the same.
- backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- // Here we take the value from the MyHomePage object that was created by
- // the App.build method, and use it to set our appbar title.
- title: Text(widget.title),
- ),
- body: Center(
- // Center is a layout widget. It takes a single child and positions it
- // in the middle of the parent.
- child: Column(
- // Column is also a layout widget. It takes a list of children and
- // arranges them vertically. By default, it sizes itself to fit its
- // children horizontally, and tries to be as tall as its parent.
- //
- // Column has various properties to control how it sizes itself and
- // how it positions its children. Here we use mainAxisAlignment to
- // center the children vertically; the main axis here is the vertical
- // axis because Columns are vertical (the cross axis would be
- // horizontal).
- //
- // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
- // action in the IDE, or press "p" in the console), to see the
- // wireframe for each widget.
- mainAxisAlignment: .center,
- children: [
- const Text('You have pushed the button this many times:'),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
- ),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: _incrementCounter,
- tooltip: 'Increment',
- child: const Icon(Icons.add),
- ),
- );
- }
-}
diff --git a/client/mobile/pubspec.lock b/client/mobile/pubspec.lock
deleted file mode 100644
index 67f625c..0000000
--- a/client/mobile/pubspec.lock
+++ /dev/null
@@ -1,213 +0,0 @@
-# Generated by pub
-# See https://dart.dev/tools/pub/glossary#lockfile
-packages:
- async:
- dependency: transitive
- description:
- name: async
- sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
- url: "https://pub.dev"
- source: hosted
- version: "2.13.0"
- boolean_selector:
- dependency: transitive
- description:
- name: boolean_selector
- sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.2"
- characters:
- dependency: transitive
- description:
- name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
- url: "https://pub.dev"
- source: hosted
- version: "1.4.0"
- clock:
- dependency: transitive
- description:
- name: clock
- sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
- url: "https://pub.dev"
- source: hosted
- version: "1.1.2"
- collection:
- dependency: transitive
- description:
- name: collection
- sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
- url: "https://pub.dev"
- source: hosted
- version: "1.19.1"
- cupertino_icons:
- dependency: "direct main"
- description:
- name: cupertino_icons
- sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
- url: "https://pub.dev"
- source: hosted
- version: "1.0.8"
- fake_async:
- dependency: transitive
- description:
- name: fake_async
- sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.3"
- flutter:
- dependency: "direct main"
- description: flutter
- source: sdk
- version: "0.0.0"
- flutter_lints:
- dependency: "direct dev"
- description:
- name: flutter_lints
- sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
- url: "https://pub.dev"
- source: hosted
- version: "6.0.0"
- flutter_test:
- dependency: "direct dev"
- description: flutter
- source: sdk
- version: "0.0.0"
- leak_tracker:
- dependency: transitive
- description:
- name: leak_tracker
- sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
- url: "https://pub.dev"
- source: hosted
- version: "11.0.2"
- leak_tracker_flutter_testing:
- dependency: transitive
- description:
- name: leak_tracker_flutter_testing
- sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
- url: "https://pub.dev"
- source: hosted
- version: "3.0.10"
- leak_tracker_testing:
- dependency: transitive
- description:
- name: leak_tracker_testing
- sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
- url: "https://pub.dev"
- source: hosted
- version: "3.0.2"
- lints:
- dependency: transitive
- description:
- name: lints
- sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
- url: "https://pub.dev"
- source: hosted
- version: "6.0.0"
- matcher:
- dependency: transitive
- description:
- name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
- url: "https://pub.dev"
- source: hosted
- version: "0.12.17"
- material_color_utilities:
- dependency: transitive
- description:
- name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
- url: "https://pub.dev"
- source: hosted
- version: "0.11.1"
- meta:
- dependency: transitive
- description:
- name: meta
- sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
- url: "https://pub.dev"
- source: hosted
- version: "1.17.0"
- path:
- dependency: transitive
- description:
- name: path
- sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
- url: "https://pub.dev"
- source: hosted
- version: "1.9.1"
- sky_engine:
- dependency: transitive
- description: flutter
- source: sdk
- version: "0.0.0"
- source_span:
- dependency: transitive
- description:
- name: source_span
- sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
- url: "https://pub.dev"
- source: hosted
- version: "1.10.1"
- stack_trace:
- dependency: transitive
- description:
- name: stack_trace
- sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
- url: "https://pub.dev"
- source: hosted
- version: "1.12.1"
- stream_channel:
- dependency: transitive
- description:
- name: stream_channel
- sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.4"
- string_scanner:
- dependency: transitive
- description:
- name: string_scanner
- sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
- url: "https://pub.dev"
- source: hosted
- version: "1.4.1"
- term_glyph:
- dependency: transitive
- description:
- name: term_glyph
- sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.2"
- test_api:
- dependency: transitive
- description:
- name: test_api
- sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
- url: "https://pub.dev"
- source: hosted
- version: "0.7.7"
- vector_math:
- dependency: transitive
- description:
- name: vector_math
- sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
- url: "https://pub.dev"
- source: hosted
- version: "2.2.0"
- vm_service:
- dependency: transitive
- description:
- name: vm_service
- sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
- url: "https://pub.dev"
- source: hosted
- version: "15.0.2"
-sdks:
- dart: ">=3.10.0-290.4.beta <4.0.0"
- flutter: ">=3.18.0-18.0.pre.54"
diff --git a/client/mobile/pubspec.yaml b/client/mobile/pubspec.yaml
deleted file mode 100644
index 8f65623..0000000
--- a/client/mobile/pubspec.yaml
+++ /dev/null
@@ -1,89 +0,0 @@
-name: nixcn
-description: "A new Flutter project."
-# The following line prevents the package from being accidentally published to
-# pub.dev using `flutter pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-# In Windows, build-name is used as the major, minor, and patch parts
-# of the product and file versions while build-number is used as the build suffix.
-version: 1.0.0+1
-
-environment:
- sdk: ^3.10.0-290.4.beta
-
-# Dependencies specify other packages that your package needs in order to work.
-# To automatically upgrade your package dependencies to the latest versions
-# consider running `flutter pub upgrade --major-versions`. Alternatively,
-# dependencies can be manually updated by changing the version numbers below to
-# the latest version available on pub.dev. To see which dependencies have newer
-# versions available, run `flutter pub outdated`.
-dependencies:
- flutter:
- sdk: flutter
-
- # The following adds the Cupertino Icons font to your application.
- # Use with the CupertinoIcons class for iOS style icons.
- cupertino_icons: ^1.0.8
-
-dev_dependencies:
- flutter_test:
- sdk: flutter
-
- # The "flutter_lints" package below contains a set of recommended lints to
- # encourage good coding practices. The lint set provided by the package is
- # activated in the `analysis_options.yaml` file located at the root of your
- # package. See that file for information about deactivating specific lint
- # rules and activating additional ones.
- flutter_lints: ^6.0.0
-
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter packages.
-flutter:
-
- # The following line ensures that the Material Icons font is
- # included with your application, so that you can use the icons in
- # the material Icons class.
- uses-material-design: true
-
- # To add assets to your application, add an assets section, like this:
- # assets:
- # - images/a_dot_burr.jpeg
- # - images/a_dot_ham.jpeg
-
- # An image asset can refer to one or more resolution-specific "variants", see
- # https://flutter.dev/to/resolution-aware-images
-
- # For details regarding adding assets from package dependencies, see
- # https://flutter.dev/to/asset-from-package
-
- # To add custom fonts to your application, add a fonts section here,
- # in this "flutter" section. Each entry in this list should have a
- # "family" key with the font family name, and a "fonts" key with a
- # list giving the asset and other descriptors for the font. For
- # example:
- # fonts:
- # - family: Schyler
- # fonts:
- # - asset: fonts/Schyler-Regular.ttf
- # - asset: fonts/Schyler-Italic.ttf
- # style: italic
- # - family: Trajan Pro
- # fonts:
- # - asset: fonts/TrajanPro.ttf
- # - asset: fonts/TrajanPro_Bold.ttf
- # weight: 700
- #
- # For details regarding fonts from package dependencies,
- # see https://flutter.dev/to/font-from-package
diff --git a/client/mobile/test/widget_test.dart b/client/mobile/test/widget_test.dart
deleted file mode 100644
index 43b8af9..0000000
--- a/client/mobile/test/widget_test.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:nixcn/main.dart';
-
-void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(const MyApp());
-
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
- });
-}
diff --git a/client/party/.gitkeep b/client/party/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/gen_exception/definitions/common.yaml b/cmd/gen_exception/definitions/common.yaml
index 211913e..7b8f653 100644
--- a/cmd/gen_exception/definitions/common.yaml
+++ b/cmd/gen_exception/definitions/common.yaml
@@ -9,3 +9,9 @@ common:
database: "00006"
missing_user_id: "00007"
user_not_found: "00008"
+ user_not_public: "00009"
+ base64_decode_failed: "00010"
+ json_decode_failed: "00011"
+ aes_encode_failed: "00012"
+ aes_decode_failed: "00013"
+ missing_event_id: "00014"
diff --git a/cmd/gen_exception/definitions/endpoint.yaml b/cmd/gen_exception/definitions/endpoint.yaml
index 86be040..3e85ab4 100644
--- a/cmd/gen_exception/definitions/endpoint.yaml
+++ b/cmd/gen_exception/definitions/endpoint.yaml
@@ -1,23 +1,41 @@
endpoint:
+ middleware: "000"
auth:
- service:
- redirect: "01"
- magic: "02"
- token: "03"
- refresh: "04"
- exchange: "05"
+ redirect: "101"
+ magic: "102"
+ token: "103"
+ refresh: "104"
+ exchange: "105"
event:
- service:
- info: "01"
- checkin: "02"
- checkin_query: "03"
- checkin_submit: "04"
+ info: "201"
+ checkin: "202"
+ checkin_query: "203"
+ checkin_submit: "204"
+ list: "205"
+ join: "206"
+ attendance: "207"
+ attendance_guide: "208"
+ create: "209"
+ update: "210"
+ delete: "211"
+ stats: "212"
user:
- service:
- info: "01"
- update: "02"
- list: "03"
- full: "04"
- create: "05"
- middleware:
- service: "01"
+ info: "301"
+ update: "302"
+ list: "303"
+ full: "304"
+ create: "305"
+ admin_update: "306"
+ stats:
+ global: "601"
+ kyc:
+ session: "401"
+ query: "402"
+ agenda:
+ submit: "501"
+ update: "502"
+ review: "503"
+ schedule: "504"
+ list: "505"
+ my_list: "506"
+ schedule_get: "507"
diff --git a/cmd/gen_exception/definitions/middleware.yaml b/cmd/gen_exception/definitions/middleware.yaml
index 159ee25..6ed7a17 100644
--- a/cmd/gen_exception/definitions/middleware.yaml
+++ b/cmd/gen_exception/definitions/middleware.yaml
@@ -3,4 +3,3 @@ middleware:
gin_logger: "901"
jwt: "902"
permission: "903"
- api_version: "904"
diff --git a/cmd/gen_exception/definitions/service.yaml b/cmd/gen_exception/definitions/service.yaml
index eb6ee05..81a2e9f 100644
--- a/cmd/gen_exception/definitions/service.yaml
+++ b/cmd/gen_exception/definitions/service.yaml
@@ -1,4 +1,37 @@
service:
- auth: "001"
- user: "002"
- event: "003"
+ endpoint: "000"
+ auth:
+ exchange: "101"
+ magic: "102"
+ redirect: "103"
+ token: "104"
+ refresh: "105"
+ user:
+ get_info: "201"
+ list: "202"
+ update_info: "203"
+ admin_update: "204"
+ stats:
+ global: "601"
+ event:
+ attendance_list: "301"
+ checkin: "302"
+ get_attendance_guide: "303"
+ get_event_info: "304"
+ join: "305"
+ list: "306"
+ create: "307"
+ update: "308"
+ delete: "309"
+ stats: "310"
+ kyc:
+ query: "401"
+ session: "402"
+ agenda:
+ submit: "501"
+ update: "502"
+ review: "503"
+ schedule: "504"
+ list: "505"
+ my_list: "506"
+ schedule_get: "507"
diff --git a/cmd/gen_exception/definitions/specific.yaml b/cmd/gen_exception/definitions/specific.yaml
index a162fe8..a0600b1 100644
--- a/cmd/gen_exception/definitions/specific.yaml
+++ b/cmd/gen_exception/definitions/specific.yaml
@@ -24,7 +24,10 @@ auth:
invalid_redirect_uri: "00003"
user:
list:
- meilisearch_failed: "00001"
+ database_failed: "00001"
+ update:
+ permission_matrix_violated: "00001"
+ permission_level_too_high: "00002"
event:
info:
not_found: "00001"
@@ -32,3 +35,40 @@ event:
gen_code_failed: "00001"
checkin_query:
record_not_found: "00001"
+ join:
+ event_invalid: "00001"
+ limit_exceeded: "00002"
+ attendance:
+ list_error: "00001"
+ create:
+ type_not_allowed: "00001"
+ update:
+ immutable_field: "00001"
+ not_owner: "00002"
+ agenda_already_published: "00003"
+ agenda_preflight_failed: "00004"
+ delete:
+ not_found: "00001"
+ not_owner: "00002"
+ stats:
+ not_owner: "00001"
+agenda:
+ submit:
+ event_started: "00001"
+ event_published: "00002"
+ pending_limit_reached: "00003"
+ update:
+ not_pending: "00001"
+ not_submitter: "00002"
+ deadline_passed: "00003"
+ review:
+ event_published: "00001"
+ schedule:
+ not_approved: "00001"
+ schedule_get:
+ not_published: "00001"
+kyc:
+ session:
+ failed: "00001"
+ query:
+ failed: "00001"
diff --git a/config.default.yaml b/config.default.yaml
index 602c7bb..efdacfa 100644
--- a/config.default.yaml
+++ b/config.default.yaml
@@ -19,10 +19,6 @@ cache:
password: ""
db: 0
service_name: nixcn-cms-redis
-search:
- host: 127.0.0.1
- api_key: ""
- service_name: nixcn-cms-meilisearch
email:
host:
port:
@@ -42,5 +38,7 @@ ttl:
kyc:
ali_access_key_id: example
ali_access_key_secret: example
+ passport_reader_public_key: example
+ passport_reader_secret: example
tracer:
otel_controller_endpoint: localhost:4317
diff --git a/config/config.go b/config/config.go
index 33aaa9d..db51526 100644
--- a/config/config.go
+++ b/config/config.go
@@ -26,12 +26,16 @@ func Init() {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
- conf := &config{}
if err := viper.ReadInConfig(); err != nil {
- // Dont generate config when using dev mode
- log.Fatalln("[Config] Can't read config!")
+ if _, ok := err.(viper.ConfigFileNotFoundError); ok {
+ log.Println("[Config] No config file found, using Env vars only.")
+ } else {
+ log.Fatalf("[Config] Fatal error reading config file: %s \n", err)
+ }
}
+
+ conf := &config{}
if err := viper.Unmarshal(conf); err != nil {
- log.Fatalln("[Condig] Can't unmarshal config!")
+ log.Fatalln("[Config] Can't unmarshal config!")
}
}
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 0000000..2dc5d41
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,68 @@
+package config
+
+import (
+ "os"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+)
+
+// ---- ConfigDir ----
+
+func TestConfigDirDefault(t *testing.T) {
+ os.Unsetenv("CONFIG_PATH")
+ assert.Equal(t, ".", ConfigDir())
+}
+
+func TestConfigDirFromEnv(t *testing.T) {
+ os.Setenv("CONFIG_PATH", "/etc/app/config")
+ defer os.Unsetenv("CONFIG_PATH")
+ assert.Equal(t, "/etc/app/config", ConfigDir())
+}
+
+// ---- TZ ----
+
+func TestTZDefault(t *testing.T) {
+ os.Unsetenv("TZ")
+ assert.Equal(t, "Asia/Shanghai", TZ())
+}
+
+func TestTZFromEnv(t *testing.T) {
+ os.Setenv("TZ", "UTC")
+ defer os.Unsetenv("TZ")
+ assert.Equal(t, "UTC", TZ())
+}
+
+// ---- Init ----
+
+func TestInitNoConfigFile(t *testing.T) {
+ viper.Reset()
+ defer viper.Reset()
+
+ // Point to a directory that has no config.yaml — Init should log a
+ // warning (ConfigFileNotFoundError) but must not call log.Fatalf.
+ os.Setenv("CONFIG_PATH", t.TempDir())
+ defer os.Unsetenv("CONFIG_PATH")
+
+ assert.NotPanics(t, func() { Init() })
+}
+
+func TestInitWithConfigFile(t *testing.T) {
+ viper.Reset()
+ defer viper.Reset()
+
+ dir := t.TempDir()
+ // Write a minimal valid config file
+ yaml := []byte("server:\n application: test-app\n address: :9090\n")
+ if err := os.WriteFile(dir+"/config.yaml", yaml, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ os.Setenv("CONFIG_PATH", dir)
+ defer os.Unsetenv("CONFIG_PATH")
+
+ assert.NotPanics(t, func() { Init() })
+ assert.Equal(t, "test-app", viper.GetString("server.application"))
+ assert.Equal(t, ":9090", viper.GetString("server.address"))
+}
diff --git a/config/types.go b/config/types.go
index 21e84aa..c582a18 100644
--- a/config/types.go
+++ b/config/types.go
@@ -4,7 +4,6 @@ type config struct {
Server server `yaml:"server"`
Database database `yaml:"database"`
Cache cache `yaml:"cache"`
- Search search `yaml:"search"`
Email email `yaml:"email"`
Secrets secrets `yaml:"secrets"`
TTL ttl `yaml:"ttl"`
@@ -39,12 +38,6 @@ type cache struct {
ServiceName string `yaml:"service_name"`
}
-type search struct {
- Host string `yaml:"host"`
- ApiKey string `yaml:"api_key"`
- ServiceName string `yaml:"service_name"`
-}
-
type email struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
@@ -68,8 +61,11 @@ type ttl struct {
}
type kyc struct {
- AliAccessKeyId string `yaml:"ali_access_key_id"`
- AliAccessKeySecret string `yaml:"ali_access_key_secret"`
+ AliAccessKeyId string `yaml:"ali_access_key_id"`
+ AliAccessKeySecret string `yaml:"ali_access_key_secret"`
+ PassportReaderEndpoint string `yaml:"passport_reader_endpoint"`
+ PassportReaderPublicKey string `yaml:"passport_reader_public_key"`
+ PassportReaderSecret string `yaml:"passport_reader_secret"`
}
type tracer struct {
diff --git a/data/agenda.go b/data/agenda.go
index 2955ef1..ec7b946 100644
--- a/data/agenda.go
+++ b/data/agenda.go
@@ -1,13 +1,245 @@
package data
-import "github.com/google/uuid"
+import (
+ "context"
+ "time"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
type Agenda struct {
- Id uint `json:"id" gorm:"primarykey;autoIncrement"`
- UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
- AgendaId uuid.UUID `json:"agenda_id" gorm:"type:uuid;uniqueIndex;not null"`
- EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
- UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
- Name string `json:"name" gorm:"type:varchar(255);not null"`
- Description string `json:"description" gorm:"type:text;not null"`
+ Id uint `json:"id" gorm:"primarykey;autoIncrement"`
+ UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
+ AgendaId uuid.UUID `json:"agenda_id" gorm:"type:uuid;uniqueIndex;not null"`
+ AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;index;not null"`
+ Name string `json:"name" gorm:"type:varchar(255);index;not null"`
+ Description string `json:"description" gorm:"type:text;not null"` // base64 encoded markdown
+ Status string `json:"status" gorm:"type:varchar(32);index;not null;default:pending"` // pending | approved | rejected
+ StartTime time.Time `json:"start_time" gorm:"index"`
+ EndTime time.Time `json:"end_time" gorm:"index"`
+}
+
+type AgendaDoc struct {
+ AgendaId uuid.UUID `json:"agenda_id"`
+ AttendanceId uuid.UUID `json:"attendance_id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Status string `json:"status"`
+ StartTime time.Time `json:"start_time"`
+ EndTime time.Time `json:"end_time"`
+}
+
+type agendaOpts struct {
+ AttendanceId *uuid.UUID
+ Name *string
+ Description *string
+ Status *string
+ StartTime *time.Time
+ EndTime *time.Time
+}
+
+type AgendaOption func(*agendaOpts)
+
+func WithAttendanceId(v uuid.UUID) AgendaOption { return func(o *agendaOpts) { o.AttendanceId = &v } }
+func WithAgendaName(v string) AgendaOption { return func(o *agendaOpts) { o.Name = &v } }
+func WithAgendaDescription(v string) AgendaOption { return func(o *agendaOpts) { o.Description = &v } }
+func WithAgendaStatus(v string) AgendaOption { return func(o *agendaOpts) { o.Status = &v } }
+func WithAgendaStartTime(v time.Time) AgendaOption { return func(o *agendaOpts) { o.StartTime = &v } }
+func WithAgendaEndTime(v time.Time) AgendaOption { return func(o *agendaOpts) { o.EndTime = &v } }
+
+func applyAgendaOpts(opts []AgendaOption) *agendaOpts {
+ o := &agendaOpts{}
+ for _, opt := range opts {
+ opt(o)
+ }
+ return o
+}
+
+func NewAgenda(opts ...AgendaOption) *Agenda {
+ o := applyAgendaOpts(opts)
+ a := &Agenda{}
+ if o.AttendanceId != nil {
+ a.AttendanceId = *o.AttendanceId
+ }
+ if o.Name != nil {
+ a.Name = *o.Name
+ }
+ if o.Description != nil {
+ a.Description = *o.Description
+ }
+ if o.Status != nil {
+ a.Status = *o.Status
+ }
+ if o.StartTime != nil {
+ a.StartTime = *o.StartTime
+ }
+ if o.EndTime != nil {
+ a.EndTime = *o.EndTime
+ }
+ return a
+}
+
+func (self *Agenda) GetByAgendaId(ctx context.Context, agendaId uuid.UUID) (*Agenda, error) {
+ var agenda Agenda
+
+ err := Database.WithContext(ctx).
+ Where("agenda_id = ?", agendaId).
+ First(&agenda).Error
+
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &agenda, nil
+}
+
+func (self *Agenda) GetListByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (*[]Agenda, error) {
+ var result []Agenda
+
+ err := Database.WithContext(ctx).
+ Model(&Agenda{}).
+ Where("attendance_id = ?", attendanceId).
+ Order("id ASC").
+ Scan(&result).Error
+
+ return &result, err
+}
+
+func (self *Agenda) GetListByEventId(ctx context.Context, eventId uuid.UUID) (*[]AgendaDoc, error) {
+ var result []AgendaDoc
+
+ err := Database.WithContext(ctx).
+ Model(&Agenda{}).
+ Select("agendas.agenda_id, agendas.attendance_id, agendas.name, agendas.description, agendas.status, agendas.start_time, agendas.end_time").
+ Joins("JOIN attendances ON attendances.attendance_id = agendas.attendance_id").
+ Where("attendances.event_id = ?", eventId).
+ Order("agendas.id ASC").
+ Scan(&result).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+func (self *Agenda) GetScheduledByEventId(ctx context.Context, eventId uuid.UUID) (*[]AgendaDoc, error) {
+ var result []AgendaDoc
+
+ zero := time.Time{}
+
+ err := Database.WithContext(ctx).
+ Model(&Agenda{}).
+ Select("agendas.agenda_id, agendas.attendance_id, agendas.name, agendas.description, agendas.status, agendas.start_time, agendas.end_time").
+ Joins("JOIN attendances ON attendances.attendance_id = agendas.attendance_id").
+ Where("attendances.event_id = ?", eventId).
+ Where("agendas.status = ?", "approved").
+ Where("agendas.start_time > ? AND agendas.end_time > ?", zero, zero).
+ Order("agendas.start_time ASC").
+ Scan(&result).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
+
+func (self *Agenda) Create(ctx context.Context) error {
+ self.UUID = uuid.New()
+ self.AgendaId = uuid.New()
+
+ err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ return tx.Create(&self).Error
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (self *Agenda) PatchByAgendaId(ctx context.Context, agendaId uuid.UUID, opts ...AgendaOption) error {
+ o := applyAgendaOpts(opts)
+ updates := make(map[string]any)
+
+ if o.AttendanceId != nil {
+ updates["attendance_id"] = *o.AttendanceId
+ }
+ if o.Name != nil {
+ updates["name"] = *o.Name
+ }
+ if o.Description != nil {
+ updates["description"] = *o.Description
+ }
+ if o.Status != nil {
+ updates["status"] = *o.Status
+ }
+ if o.StartTime != nil {
+ updates["start_time"] = *o.StartTime
+ }
+ if o.EndTime != nil {
+ updates["end_time"] = *o.EndTime
+ }
+
+ if len(updates) == 0 {
+ return nil
+ }
+
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Model(&Agenda{}).
+ Where("agenda_id = ?", agendaId).
+ Updates(updates).Error; err != nil {
+ return err
+ }
+ return tx.Where("agenda_id = ?", agendaId).First(self).Error
+ })
+}
+
+func (self *Agenda) CountByEventId(ctx context.Context, eventId uuid.UUID) (int64, error) {
+ var count int64
+
+ err := Database.WithContext(ctx).
+ Model(&Agenda{}).
+ Joins("JOIN attendances ON attendances.attendance_id = agendas.attendance_id").
+ Where("attendances.event_id = ?", eventId).
+ Count(&count).Error
+
+ if err != nil {
+ return 0, err
+ }
+
+ return count, nil
+}
+
+func (self *Agenda) CountPendingByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (int64, error) {
+ var count int64
+
+ err := Database.WithContext(ctx).
+ Model(&Agenda{}).
+ Where("attendance_id = ? AND status = ?", attendanceId, "pending").
+ Count(&count).Error
+
+ if err != nil {
+ return 0, err
+ }
+
+ return count, nil
+}
+
+func (self *Agenda) Delete(ctx context.Context, agendaId uuid.UUID) error {
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ result := tx.Where("agenda_id = ?", agendaId).Delete(&Agenda{})
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 {
+ return gorm.ErrRecordNotFound
+ }
+ return nil
+ })
}
diff --git a/data/agenda_test.go b/data/agenda_test.go
new file mode 100644
index 0000000..15b57be
--- /dev/null
+++ b/data/agenda_test.go
@@ -0,0 +1,233 @@
+package data_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func seedAgendaFixture(t *testing.T, ctx context.Context) (eventId, attendanceId uuid.UUID) {
+ t.Helper()
+ eId, uId := seedEventAndUser(t, ctx)
+ a := seedAttendance(t, ctx, eId, uId)
+ return eId, a.AttendanceId
+}
+
+func TestAgendaCreate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, attId := seedAgendaFixture(t, ctx)
+
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName("My Talk"),
+ data.WithAgendaDescription("base64content"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+ assert.NotEqual(t, uuid.Nil, ag.AgendaId)
+}
+
+func TestAgendaGetByAgendaId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, attId := seedAgendaFixture(t, ctx)
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName("Talk A"),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+
+ got, err := new(data.Agenda).GetByAgendaId(ctx, ag.AgendaId)
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ assert.Equal(t, "Talk A", got.Name)
+}
+
+func TestAgendaGetByAgendaIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ got, err := new(data.Agenda).GetByAgendaId(ctx, uuid.New())
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestAgendaGetListByAttendanceId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, attId := seedAgendaFixture(t, ctx)
+
+ for i := 0; i < 3; i++ {
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName(uuid.New().String()),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+ }
+
+ list, err := new(data.Agenda).GetListByAttendanceId(ctx, attId)
+ require.NoError(t, err)
+ assert.Len(t, *list, 3)
+}
+
+func TestAgendaGetListByEventId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ evId, attId := seedAgendaFixture(t, ctx)
+
+ for i := 0; i < 2; i++ {
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName(uuid.New().String()),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("approved"),
+ )
+ require.NoError(t, ag.Create(ctx))
+ }
+
+ list, err := new(data.Agenda).GetListByEventId(ctx, evId)
+ require.NoError(t, err)
+ assert.Len(t, *list, 2)
+}
+
+func TestAgendaGetScheduledByEventId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ evId, attId := seedAgendaFixture(t, ctx)
+
+ now := time.Now()
+ ag1 := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName("Scheduled Talk"),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("approved"),
+ data.WithAgendaStartTime(now.Add(time.Hour)),
+ data.WithAgendaEndTime(now.Add(2*time.Hour)),
+ )
+ require.NoError(t, ag1.Create(ctx))
+
+ ag2 := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName("Pending Talk"),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag2.Create(ctx))
+
+ list, err := new(data.Agenda).GetScheduledByEventId(ctx, evId)
+ require.NoError(t, err)
+ require.Len(t, *list, 1)
+ assert.Equal(t, ag1.AgendaId, (*list)[0].AgendaId)
+}
+
+func TestAgendaPatch(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, attId := seedAgendaFixture(t, ctx)
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName("Original"),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+
+ require.NoError(t, ag.PatchByAgendaId(ctx, ag.AgendaId,
+ data.WithAgendaName("Updated"),
+ data.WithAgendaStatus("approved"),
+ ))
+ assert.Equal(t, "Updated", ag.Name)
+ assert.Equal(t, "approved", ag.Status)
+}
+
+func TestAgendaCountPendingByAttendanceId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, attId := seedAgendaFixture(t, ctx)
+
+ for _, status := range []string{"pending", "pending", "approved"} {
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName(uuid.New().String()),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus(status),
+ )
+ require.NoError(t, ag.Create(ctx))
+ }
+
+ count, err := new(data.Agenda).CountPendingByAttendanceId(ctx, attId)
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), count)
+}
+
+func TestAgendaDelete(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, attId := seedAgendaFixture(t, ctx)
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName("Delete me"),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+
+ require.NoError(t, new(data.Agenda).Delete(ctx, ag.AgendaId))
+
+ got, err := new(data.Agenda).GetByAgendaId(ctx, ag.AgendaId)
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestAgendaCountByEventId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ evId, attId := seedAgendaFixture(t, ctx)
+
+ for _, status := range []string{"pending", "approved", "rejected"} {
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attId),
+ data.WithAgendaName(uuid.New().String()),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus(status),
+ )
+ require.NoError(t, ag.Create(ctx))
+ }
+
+ count, err := new(data.Agenda).CountByEventId(ctx, evId)
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), count)
+}
+
+func TestAgendaCountByEventIdEmpty(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ count, err := new(data.Agenda).CountByEventId(ctx, ev.EventId)
+ require.NoError(t, err)
+ assert.Equal(t, int64(0), count)
+}
diff --git a/data/attendance.go b/data/attendance.go
index 09d7e1c..40db38d 100644
--- a/data/attendance.go
+++ b/data/attendance.go
@@ -9,7 +9,6 @@ import (
"time"
"github.com/google/uuid"
- "github.com/meilisearch/meilisearch-go"
"github.com/spf13/viper"
"gorm.io/gorm"
)
@@ -20,26 +19,69 @@ type Attendance struct {
AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;uniqueIndex;not null"`
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
+ KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
Role string `json:"role" gorm:"type:varchar(255);not null"`
- State string `json:"state" gorm:"type:varchar(255);not null"`
- KycInfo string `json:"kyc_info" gorm:"type:text"`
+ State string `json:"state" gorm:"type:varchar(255);not null"` // suspended | out_of_limit | success
+ JoinedAt time.Time `json:"joined_at" gorm:"autoCreateTime"`
CheckinAt time.Time `json:"checkin_at"`
}
-type AttendanceSearchDoc struct {
- AttendanceId string `json:"attendance_id"`
- EventId string `json:"event_id"`
- UserId string `json:"user_id"`
- Role string `json:"role"`
- CheckinAt time.Time `json:"checkin_at"`
+type attendanceOpts struct {
+ EventId *uuid.UUID
+ UserId *uuid.UUID
+ KycId *uuid.UUID
+ Role *string
+ State *string
+ CheckinAt *time.Time
}
-func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) {
- var checkin Attendance
+type AttendanceOption func(*attendanceOpts)
+
+func WithEventId(v uuid.UUID) AttendanceOption { return func(o *attendanceOpts) { o.EventId = &v } }
+func WithUserId(v uuid.UUID) AttendanceOption { return func(o *attendanceOpts) { o.UserId = &v } }
+func WithKycId(v uuid.UUID) AttendanceOption { return func(o *attendanceOpts) { o.KycId = &v } }
+func WithRole(v string) AttendanceOption { return func(o *attendanceOpts) { o.Role = &v } }
+func WithState(v string) AttendanceOption { return func(o *attendanceOpts) { o.State = &v } }
+func WithCheckinAt(v time.Time) AttendanceOption { return func(o *attendanceOpts) { o.CheckinAt = &v } }
+
+func applyAttendanceOpts(opts []AttendanceOption) *attendanceOpts {
+ o := &attendanceOpts{}
+ for _, opt := range opts {
+ opt(o)
+ }
+ return o
+}
+
+func NewAttendance(opts ...AttendanceOption) *Attendance {
+ o := applyAttendanceOpts(opts)
+ a := &Attendance{}
+ if o.EventId != nil {
+ a.EventId = *o.EventId
+ }
+ if o.UserId != nil {
+ a.UserId = *o.UserId
+ }
+ if o.KycId != nil {
+ a.KycId = *o.KycId
+ }
+ if o.Role != nil {
+ a.Role = *o.Role
+ }
+ if o.State != nil {
+ a.State = *o.State
+ }
+ if o.CheckinAt != nil {
+ a.CheckinAt = *o.CheckinAt
+ }
+ return a
+}
+
+func (self *Attendance) GetAttendanceByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (*Attendance, error) {
+ var attendance Attendance
err := Database.WithContext(ctx).
- Where("user_id = ? AND event_id = ?", userId, eventId).
- First(&checkin).Error
+ Where("attendance_id = ?", attendanceId).
+ First(&attendance).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
@@ -48,7 +90,24 @@ func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.
return nil, err
}
- return &checkin, err
+ return &attendance, nil
+}
+
+func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) {
+ var attendance Attendance
+
+ err := Database.WithContext(ctx).
+ Where("user_id = ? AND event_id = ?", userId, eventId).
+ First(&attendance).Error
+
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &attendance, err
}
type AttendanceUsers struct {
@@ -57,6 +116,13 @@ type AttendanceUsers struct {
CheckinAt time.Time `json:"checkin_at"`
}
+type AttendanceUserProfile struct {
+ AttendanceId uuid.UUID
+ UserId uuid.UUID
+ Nickname string
+ Username string
+}
+
func (self *Attendance) GetUsersByEventID(ctx context.Context, eventID uuid.UUID) (*[]AttendanceUsers, error) {
var result []AttendanceUsers
@@ -70,6 +136,28 @@ func (self *Attendance) GetUsersByEventID(ctx context.Context, eventID uuid.UUID
return &result, err
}
+func (self *Attendance) GetUserProfilesByAttendanceIds(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]AttendanceUserProfile, error) {
+ var rows []AttendanceUserProfile
+
+ err := Database.WithContext(ctx).
+ Table("attendances").
+ Select("attendances.attendance_id, attendances.user_id, users.nickname, users.username").
+ Joins("JOIN users ON users.user_id = attendances.user_id").
+ Where("attendances.attendance_id IN ?", ids).
+ Scan(&rows).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[uuid.UUID]AttendanceUserProfile, len(rows))
+ for _, r := range rows {
+ result[r.AttendanceId] = r
+ }
+
+ return result, nil
+}
+
type AttendanceEvent struct {
EventId uuid.UUID `json:"event_id"`
CheckinAt time.Time `json:"checkin_at"`
@@ -88,117 +176,267 @@ func (self *Attendance) GetEventsByUserID(ctx context.Context, userID uuid.UUID)
return &result, err
}
-func (self *Attendance) Create(ctx context.Context) error {
+func (self *Attendance) Create(ctx context.Context) (uuid.UUID, error) {
self.UUID = uuid.New()
self.AttendanceId = uuid.New()
- // DB transaction for strong consistency
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Create(&self).Error; err != nil {
- return err
- }
- return nil
+ return tx.Create(&self).Error
})
if err != nil {
- return err
+ return uuid.Nil, err
}
- if err := self.UpdateSearchIndex(ctx); err != nil {
- return err
- }
-
- return nil
+ return self.AttendanceId, nil
}
-func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID, checkinTime *time.Time) (*Attendance, error) {
- var attendance Attendance
+func (self *Attendance) PatchByAttendanceId(ctx context.Context, attendanceId uuid.UUID, opts ...AttendanceOption) (*Attendance, error) {
+ o := applyAttendanceOpts(opts)
+ updates := make(map[string]any)
+ if o.EventId != nil {
+ updates["event_id"] = *o.EventId
+ }
+ if o.UserId != nil {
+ updates["user_id"] = *o.UserId
+ }
+ if o.KycId != nil {
+ updates["kyc_id"] = *o.KycId
+ }
+ if o.Role != nil {
+ updates["role"] = *o.Role
+ }
+ if o.State != nil {
+ updates["state"] = *o.State
+ }
+ if o.CheckinAt != nil {
+ updates["checkin_at"] = *o.CheckinAt
+ }
+
+ if len(updates) == 0 {
+ return self, nil
+ }
+
+ var updated Attendance
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- // Lock the row for update
- if err := tx.
- Where("attendance_id = ?", attendanceId).
- First(&attendance).Error; err != nil {
+ if err := tx.Where("attendance_id = ?", attendanceId).First(&updated).Error; err != nil {
return err
}
-
- updates := map[string]any{}
-
- if checkinTime != nil {
- updates["checkin_at"] = *checkinTime
- }
-
- if len(updates) == 0 {
- return nil
- }
-
- if err := tx.
- Model(&attendance).
- Updates(updates).Error; err != nil {
+ if err := tx.Model(&updated).Updates(updates).Error; err != nil {
return err
}
-
- // Reload to ensure struct is up to date
- return tx.
- Where("attendance_id = ?", attendanceId).
- First(&attendance).Error
+ return tx.Where("attendance_id = ?", attendanceId).First(&updated).Error
})
+ if err != nil {
+ return nil, err
+ }
+
+ return &updated, nil
+}
+
+func (self *Attendance) GetAttendanceListByEventId(ctx context.Context, eventId uuid.UUID) (*[]Attendance, error) {
+ var result []Attendance
+
+ err := Database.WithContext(ctx).
+ Where("event_id = ?", eventId).
+ Order("checkin_at DESC").
+ Find(&result).Error
+
+ return &result, err
+}
+
+func (self *Attendance) GetJoinedEventIDs(ctx context.Context, userId uuid.UUID, eventIds []uuid.UUID) (map[uuid.UUID]bool, error) {
+ joinedMap := make(map[uuid.UUID]bool)
+
+ if len(eventIds) == 0 {
+ return joinedMap, nil
+ }
+
+ var foundEventIds []uuid.UUID
+
+ err := Database.WithContext(ctx).
+ Model(&Attendance{}).
+ Where("user_id = ? AND event_id IN ?", userId, eventIds).
+ Pluck("event_id", &foundEventIds).Error
if err != nil {
return nil, err
}
- // Sync to MeiliSearch (eventual consistency)
- if err := attendance.UpdateSearchIndex(ctx); err != nil {
+ for _, id := range foundEventIds {
+ joinedMap[id] = true
+ }
+
+ return joinedMap, nil
+}
+
+func (self *Attendance) GetCheckedInEventIDs(ctx context.Context, userId uuid.UUID, eventIds []uuid.UUID) (map[uuid.UUID]bool, error) {
+ checkedInMap := make(map[uuid.UUID]bool)
+
+ if len(eventIds) == 0 {
+ return checkedInMap, nil
+ }
+
+ var foundEventIds []uuid.UUID
+
+ err := Database.WithContext(ctx).
+ Model(&Attendance{}).
+ Where("user_id = ? AND event_id IN ?", userId, eventIds).
+ Where("checkin_at > ?", time.Time{}).
+ Pluck("event_id", &foundEventIds).Error
+
+ if err != nil {
return nil, err
}
- return &attendance, nil
-}
-
-func (self *Attendance) SearchUsersByEvent(ctx context.Context, eventID string) (*meilisearch.SearchResponse, error) {
- index := MeiliSearch.Index("attendance")
-
- return index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
- Filter: "event_id = \"" + eventID + "\"",
- Sort: []string{"checkin_at:asc"},
- })
-}
-
-func (self *Attendance) SearchEventsByUser(ctx context.Context, userID string) (*meilisearch.SearchResponse, error) {
- index := MeiliSearch.Index("attendance")
-
- return index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
- Filter: "user_id = \"" + userID + "\"",
- Sort: []string{"checkin_at:asc"},
- })
-}
-
-func (self *Attendance) UpdateSearchIndex(ctx context.Context) error {
- doc := AttendanceSearchDoc{
- AttendanceId: self.AttendanceId.String(),
- EventId: self.EventId.String(),
- UserId: self.UserId.String(),
- CheckinAt: self.CheckinAt,
+ for _, id := range foundEventIds {
+ checkedInMap[id] = true
}
- index := MeiliSearch.Index("attendance")
-
- primaryKey := "attendance_id"
- opts := &meilisearch.DocumentOptions{
- PrimaryKey: &primaryKey,
- }
-
- if _, err := index.UpdateDocumentsWithContext(ctx, []AttendanceSearchDoc{doc}, opts); err != nil {
- return err
- }
-
- return nil
+ return checkedInMap, nil
}
-func (self *Attendance) DeleteSearchIndex(ctx context.Context) error {
- index := MeiliSearch.Index("attendance")
- _, err := index.DeleteDocumentWithContext(ctx, self.AttendanceId.String(), nil)
- return err
+type AttendanceListFilter struct {
+ EventId uuid.UUID
+ Name string // substring match on users.nickname
+ KycStatus string // "with_kyc" | "without_kyc" | ""
+ SortBy string // "" | "joined_at" (default) | "checkin_at" | "id"
+ SortOrder string // "asc" | "desc"
+ Limit int
+ Offset int
+}
+
+func (self *Attendance) GetAttendanceListFiltered(ctx context.Context, filter AttendanceListFilter) (*[]Attendance, int64, error) {
+ var results []Attendance
+ var total int64
+
+ base := Database.WithContext(ctx).
+ Table("attendances").
+ Joins("JOIN users ON users.user_id = attendances.user_id").
+ Where("attendances.event_id = ?", filter.EventId)
+
+ if filter.Name != "" {
+ base = base.Where("(users.nickname LIKE ? OR users.username LIKE ?)",
+ "%"+filter.Name+"%", "%"+filter.Name+"%")
+ }
+
+ switch filter.KycStatus {
+ case "with_kyc":
+ base = base.Where("attendances.kyc_id IS NOT NULL AND attendances.kyc_id != ?", uuid.Nil)
+ case "without_kyc":
+ base = base.Where("(attendances.kyc_id IS NULL OR attendances.kyc_id = ?)", uuid.Nil)
+ }
+
+ if err := base.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+
+ sortField := "attendances.joined_at"
+ switch filter.SortBy {
+ case "id":
+ sortField = "attendances.id"
+ case "checkin_at":
+ sortField = "attendances.checkin_at"
+ }
+ sortOrder := "DESC"
+ if filter.SortOrder == "asc" {
+ sortOrder = "ASC"
+ }
+
+ err := base.
+ Select("attendances.*").
+ Order(sortField + " " + sortOrder).
+ Limit(filter.Limit).
+ Offset(filter.Offset).
+ Find(&results).Error
+
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return &results, total, nil
+}
+
+func (self *Attendance) CountWithKycByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
+ var count int64
+
+ err := Database.WithContext(ctx).
+ Model(&Attendance{}).
+ Where("event_id = ? AND kyc_id != ?", eventID, uuid.Nil).
+ Count(&count).Error
+
+ if err != nil {
+ return 0, err
+ }
+
+ return count, nil
+}
+
+type eventCount struct {
+ EventId uuid.UUID
+ Count int64
+}
+
+func (self *Attendance) CountUsersByEventIDs(ctx context.Context, eventIDs []uuid.UUID) (map[uuid.UUID]int64, error) {
+ result := make(map[uuid.UUID]int64)
+ if len(eventIDs) == 0 {
+ return result, nil
+ }
+
+ var rows []eventCount
+ err := Database.WithContext(ctx).
+ Model(&Attendance{}).
+ Select("event_id, COUNT(*) as count").
+ Where("event_id IN ?", eventIDs).
+ Group("event_id").
+ Scan(&rows).Error
+ if err != nil {
+ return nil, err
+ }
+
+ for _, r := range rows {
+ result[r.EventId] = r.Count
+ }
+ return result, nil
+}
+
+func (self *Attendance) CountCheckedInUsersByEventIDs(ctx context.Context, eventIDs []uuid.UUID) (map[uuid.UUID]int64, error) {
+ result := make(map[uuid.UUID]int64)
+ if len(eventIDs) == 0 {
+ return result, nil
+ }
+
+ var rows []eventCount
+ err := Database.WithContext(ctx).
+ Model(&Attendance{}).
+ Select("event_id, COUNT(*) as count").
+ Where("event_id IN ? AND checkin_at IS NOT NULL AND checkin_at > ?", eventIDs, time.Time{}).
+ Group("event_id").
+ Scan(&rows).Error
+ if err != nil {
+ return nil, err
+ }
+
+ for _, r := range rows {
+ result[r.EventId] = r.Count
+ }
+ return result, nil
+}
+
+func (self *Attendance) CountUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
+ m, err := self.CountUsersByEventIDs(ctx, []uuid.UUID{eventID})
+ if err != nil {
+ return 0, err
+ }
+ return m[eventID], nil
+}
+
+func (self *Attendance) CountCheckedInUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
+ m, err := self.CountCheckedInUsersByEventIDs(ctx, []uuid.UUID{eventID})
+ if err != nil {
+ return 0, err
+ }
+ return m[eventID], nil
}
func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) {
@@ -247,16 +485,18 @@ func (self *Attendance) VerifyCheckinCode(ctx context.Context, checkinCode strin
return err
}
- attendanceData, err := self.GetAttendance(ctx, userId, eventId)
+ attendanceRecord, err := self.GetAttendance(ctx, userId, eventId)
if err != nil {
return err
}
- time := time.Now()
- _, err = self.Update(ctx, attendanceData.AttendanceId, &time)
- if err != nil {
+ if _, err = self.PatchByAttendanceId(ctx, attendanceRecord.AttendanceId,
+ WithCheckinAt(time.Now()),
+ ); err != nil {
return err
}
+ _ = Redis.Del(ctx, "checkin_code:"+checkinCode).Err()
+
return nil
}
diff --git a/data/attendance_test.go b/data/attendance_test.go
new file mode 100644
index 0000000..488d800
--- /dev/null
+++ b/data/attendance_test.go
@@ -0,0 +1,481 @@
+package data_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func seedEventAndUser(t *testing.T, ctx context.Context) (eventId, userId uuid.UUID) {
+ t.Helper()
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+
+ return ev.EventId, u.UserId
+}
+
+func seedAttendance(t *testing.T, ctx context.Context, eventId, userId uuid.UUID) *data.Attendance {
+ t.Helper()
+ a := data.NewAttendance(
+ data.WithEventId(eventId),
+ data.WithUserId(userId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := a.Create(ctx)
+ require.NoError(t, err)
+ return a
+}
+
+func TestAttendanceCreate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ eventId, userId := seedEventAndUser(t, ctx)
+ a := seedAttendance(t, ctx, eventId, userId)
+ assert.NotEqual(t, uuid.Nil, a.AttendanceId)
+}
+
+func TestAttendanceGetAttendance(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ eventId, userId := seedEventAndUser(t, ctx)
+ seedAttendance(t, ctx, eventId, userId)
+
+ got, err := new(data.Attendance).GetAttendance(ctx, userId, eventId)
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ assert.Equal(t, eventId, got.EventId)
+ assert.Equal(t, userId, got.UserId)
+}
+
+func TestAttendanceGetAttendanceNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ got, err := new(data.Attendance).GetAttendance(ctx, uuid.New(), uuid.New())
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestAttendancePatch(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ eventId, userId := seedEventAndUser(t, ctx)
+ a := seedAttendance(t, ctx, eventId, userId)
+
+ now := time.Now().Truncate(time.Second)
+ updated, err := new(data.Attendance).PatchByAttendanceId(ctx, a.AttendanceId,
+ data.WithState("suspended"),
+ data.WithCheckinAt(now),
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "suspended", updated.State)
+}
+
+func TestAttendanceCountByEventID(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i := 0; i < 3; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ seedAttendance(t, ctx, ev.EventId, u.UserId)
+ }
+
+ count, err := new(data.Attendance).CountUsersByEventID(ctx, ev.EventId)
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), count)
+}
+
+func TestAttendanceGetJoinedEventIDs(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev1 := makeEvent(owner)
+ require.NoError(t, ev1.Create(ctx))
+ ev2 := makeEvent(owner)
+ require.NoError(t, ev2.Create(ctx))
+
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ seedAttendance(t, ctx, ev1.EventId, u.UserId)
+
+ joined, err := new(data.Attendance).GetJoinedEventIDs(ctx, u.UserId, []uuid.UUID{ev1.EventId, ev2.EventId})
+ require.NoError(t, err)
+ assert.True(t, joined[ev1.EventId])
+ assert.False(t, joined[ev2.EventId])
+}
+
+func TestAttendanceGenAndVerifyCheckinCode(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ eventId, userId := seedEventAndUser(t, ctx)
+ a := seedAttendance(t, ctx, eventId, userId)
+
+ code, err := a.GenCheckinCode(ctx, a.EventId)
+ require.NoError(t, err)
+ require.NotNil(t, code)
+ assert.Len(t, *code, 6)
+
+ require.NoError(t, new(data.Attendance).VerifyCheckinCode(ctx, *code))
+
+ got, err := new(data.Attendance).GetAttendance(ctx, userId, eventId)
+ require.NoError(t, err)
+ assert.False(t, got.CheckinAt.IsZero())
+}
+
+func TestAttendanceVerifyCheckinCodeInvalid(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ err := new(data.Attendance).VerifyCheckinCode(ctx, "000000")
+ require.Error(t, err)
+}
+
+func TestAttendanceGetByAttendanceId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ eventId, userId := seedEventAndUser(t, ctx)
+ a := seedAttendance(t, ctx, eventId, userId)
+
+ got, err := new(data.Attendance).GetAttendanceByAttendanceId(ctx, a.AttendanceId)
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ assert.Equal(t, a.AttendanceId, got.AttendanceId)
+ assert.Equal(t, eventId, got.EventId)
+ assert.Equal(t, userId, got.UserId)
+}
+
+func TestAttendanceGetByAttendanceIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ got, err := new(data.Attendance).GetAttendanceByAttendanceId(ctx, uuid.New())
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestAttendanceGetUsersByEventID(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i := 0; i < 3; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ seedAttendance(t, ctx, ev.EventId, u.UserId)
+ }
+
+ users, err := new(data.Attendance).GetUsersByEventID(ctx, ev.EventId)
+ require.NoError(t, err)
+ require.NotNil(t, users)
+ assert.Len(t, *users, 3)
+}
+
+func TestAttendanceGetEventsByUserID(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev1 := makeEvent(owner)
+ require.NoError(t, ev1.Create(ctx))
+ ev2 := makeEvent(owner)
+ require.NoError(t, ev2.Create(ctx))
+
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ seedAttendance(t, ctx, ev1.EventId, u.UserId)
+ seedAttendance(t, ctx, ev2.EventId, u.UserId)
+
+ events, err := new(data.Attendance).GetEventsByUserID(ctx, u.UserId)
+ require.NoError(t, err)
+ require.NotNil(t, events)
+ assert.Len(t, *events, 2)
+}
+
+func TestAttendanceGetAttendanceListByEventId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i := 0; i < 4; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ seedAttendance(t, ctx, ev.EventId, u.UserId)
+ }
+
+ list, err := new(data.Attendance).GetAttendanceListByEventId(ctx, ev.EventId)
+ require.NoError(t, err)
+ require.NotNil(t, list)
+ assert.Len(t, *list, 4)
+}
+
+func TestAttendanceGetCheckedInEventIDs(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev1 := makeEvent(owner)
+ require.NoError(t, ev1.Create(ctx))
+ ev2 := makeEvent(owner)
+ require.NoError(t, ev2.Create(ctx))
+
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+
+ a1 := seedAttendance(t, ctx, ev1.EventId, u.UserId)
+ _, err := new(data.Attendance).PatchByAttendanceId(ctx, a1.AttendanceId, data.WithCheckinAt(time.Now()))
+ require.NoError(t, err)
+
+ seedAttendance(t, ctx, ev2.EventId, u.UserId)
+
+ checkedIn, err := new(data.Attendance).GetCheckedInEventIDs(ctx, u.UserId, []uuid.UUID{ev1.EventId, ev2.EventId})
+ require.NoError(t, err)
+ assert.True(t, checkedIn[ev1.EventId])
+ assert.False(t, checkedIn[ev2.EventId])
+}
+
+func TestAttendanceCountCheckedInUsersByEventID(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i := 0; i < 3; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ a := seedAttendance(t, ctx, ev.EventId, u.UserId)
+ if i < 2 {
+ _, err := new(data.Attendance).PatchByAttendanceId(ctx, a.AttendanceId, data.WithCheckinAt(time.Now()))
+ require.NoError(t, err)
+ }
+ }
+
+ count, err := new(data.Attendance).CountCheckedInUsersByEventID(ctx, ev.EventId)
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), count)
+}
+
+func TestAttendanceCountWithKycByEventID(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i := 0; i < 3; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ kycId := uuid.Nil
+ if i < 2 {
+ kycId = uuid.New()
+ }
+ a := data.NewAttendance(
+ data.WithEventId(ev.EventId),
+ data.WithUserId(u.UserId),
+ data.WithKycId(kycId),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := a.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ count, err := new(data.Attendance).CountWithKycByEventID(ctx, ev.EventId)
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), count)
+}
+
+func TestAttendanceGetAttendanceListFiltered(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i := 0; i < 5; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithNickname(fmt.Sprintf("FilterUser%d", i)),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ kycId := uuid.Nil
+ if i < 3 {
+ kycId = uuid.New()
+ }
+ a := data.NewAttendance(
+ data.WithEventId(ev.EventId),
+ data.WithUserId(u.UserId),
+ data.WithKycId(kycId),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := a.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ t.Run("no_filters_returns_all", func(t *testing.T) {
+ list, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, data.AttendanceListFilter{
+ EventId: ev.EventId,
+ Limit: 20,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(5), total)
+ assert.Len(t, *list, 5)
+ })
+
+ t.Run("with_kyc_filter", func(t *testing.T) {
+ list, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, data.AttendanceListFilter{
+ EventId: ev.EventId,
+ KycStatus: "with_kyc",
+ Limit: 20,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), total)
+ assert.Len(t, *list, 3)
+ })
+
+ t.Run("without_kyc_filter", func(t *testing.T) {
+ list, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, data.AttendanceListFilter{
+ EventId: ev.EventId,
+ KycStatus: "without_kyc",
+ Limit: 20,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), total)
+ assert.Len(t, *list, 2)
+ })
+
+ t.Run("name_filter", func(t *testing.T) {
+ list, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, data.AttendanceListFilter{
+ EventId: ev.EventId,
+ Name: "FilterUser1",
+ Limit: 20,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(1), total)
+ assert.Len(t, *list, 1)
+ })
+
+ t.Run("pagination", func(t *testing.T) {
+ list, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, data.AttendanceListFilter{
+ EventId: ev.EventId,
+ Limit: 2,
+ Offset: 0,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(5), total)
+ assert.Len(t, *list, 2)
+ })
+
+ t.Run("sort_by_id_asc", func(t *testing.T) {
+ list, _, err := new(data.Attendance).GetAttendanceListFiltered(ctx, data.AttendanceListFilter{
+ EventId: ev.EventId,
+ SortBy: "id",
+ SortOrder: "asc",
+ Limit: 20,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, list)
+ items := *list
+ for i := 1; i < len(items); i++ {
+ assert.GreaterOrEqual(t, items[i].Id, items[i-1].Id)
+ }
+ })
+}
+
+func TestAttendanceCheckinCodeExpiry(t *testing.T) {
+ mr := testutil.Setup(t)
+ ctx := context.Background()
+
+ eventId, userId := seedEventAndUser(t, ctx)
+ a := seedAttendance(t, ctx, eventId, userId)
+
+ code, err := a.GenCheckinCode(ctx, a.EventId)
+ require.NoError(t, err)
+ require.NotNil(t, code)
+
+ mr.FastForward(10 * time.Minute)
+
+ err = new(data.Attendance).VerifyCheckinCode(ctx, *code)
+ require.Error(t, err, "expired code should not be valid")
+}
+
+func TestAttendanceGetCheckedInEventIDsEmpty(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ result, err := new(data.Attendance).GetCheckedInEventIDs(ctx, uuid.New(), []uuid.UUID{})
+ require.NoError(t, err)
+ assert.Empty(t, result)
+}
diff --git a/data/client_test.go b/data/client_test.go
new file mode 100644
index 0000000..09226d5
--- /dev/null
+++ b/data/client_test.go
@@ -0,0 +1,96 @@
+package data_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func TestClientCreate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ client, err := new(data.Client).Create(ctx, &data.ClientParams{
+ ClientId: "my-app",
+ ClientName: "My Application",
+ RedirectUri: []string{"http://localhost/callback"},
+ })
+ require.NoError(t, err)
+ assert.Equal(t, "my-app", client.ClientId)
+ assert.NotEmpty(t, client.ClientSecret)
+}
+
+func TestClientGetByClientId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, err := new(data.Client).Create(ctx, &data.ClientParams{
+ ClientId: "find-me",
+ ClientName: "Find Me App",
+ RedirectUri: []string{"http://example.com/cb"},
+ })
+ require.NoError(t, err)
+
+ found, err := new(data.Client).GetClientByClientId(ctx, "find-me")
+ require.NoError(t, err)
+ require.NotNil(t, found)
+ assert.Equal(t, "find-me", found.ClientId)
+}
+
+func TestClientGetByClientIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ _, err := new(data.Client).GetClientByClientId(ctx, "no-such-client")
+ require.Error(t, err)
+}
+
+func TestClientValidateRedirectURIMatch(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ c, err := new(data.Client).Create(ctx, &data.ClientParams{
+ ClientId: "redir-test",
+ ClientName: "Redir Test",
+ RedirectUri: []string{"http://localhost/callback", "https://example.com/auth"},
+ })
+ require.NoError(t, err)
+
+ assert.NoError(t, c.ValidateRedirectURI("http://localhost/callback"))
+ assert.NoError(t, c.ValidateRedirectURI("http://localhost/callback?extra=param"))
+ assert.NoError(t, c.ValidateRedirectURI("https://example.com/auth"))
+}
+
+func TestClientValidateRedirectURIMismatch(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ c, err := new(data.Client).Create(ctx, &data.ClientParams{
+ ClientId: "strict-redir",
+ ClientName: "Strict Redir",
+ RedirectUri: []string{"http://localhost/callback"},
+ })
+ require.NoError(t, err)
+
+ assert.Error(t, c.ValidateRedirectURI("http://evil.com/steal"))
+}
+
+func TestClientGetDecryptedSecret(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ c, err := new(data.Client).Create(ctx, &data.ClientParams{
+ ClientId: "secret-test",
+ ClientName: "Secret Test",
+ RedirectUri: []string{"http://localhost/cb"},
+ })
+ require.NoError(t, err)
+
+ secret, err := c.GetDecryptedSecret()
+ require.NoError(t, err)
+ assert.NotEmpty(t, secret)
+}
diff --git a/data/data.go b/data/data.go
index a1562a6..e4ba60b 100644
--- a/data/data.go
+++ b/data/data.go
@@ -7,7 +7,6 @@ import (
"log/slog"
- "github.com/meilisearch/meilisearch-go"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"gorm.io/gorm"
@@ -15,7 +14,6 @@ import (
var Database *gorm.DB
var Redis redis.UniversalClient
-var MeiliSearch meilisearch.ServiceManager
func Init(ctx context.Context) {
// Init database
@@ -40,7 +38,7 @@ func Init(ctx context.Context) {
}
// Auto migrate
- err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{})
+ err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{}, &Kyc{}, &Agenda{})
if err != nil {
slog.ErrorContext(ctx, "[Database] Error migrating database!", "err", err)
os.Exit(1)
@@ -62,12 +60,4 @@ func Init(ctx context.Context) {
os.Exit(1)
}
Redis = rdb
-
- // Init meilisearch
- mDSN := drivers.MeiliDSN{
- Host: viper.GetString("search.host"),
- ApiKey: viper.GetString("search.api_key"),
- }
- mdb := drivers.MeiliSearch(mDSN)
- MeiliSearch = mdb
}
diff --git a/data/drivers/meilisearch.go b/data/drivers/meilisearch.go
deleted file mode 100644
index eb50daf..0000000
--- a/data/drivers/meilisearch.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package drivers
-
-import (
- "fmt"
- "net/http"
-
- "github.com/meilisearch/meilisearch-go"
- "github.com/spf13/viper"
- "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
-)
-
-func MeiliSearch(dsn MeiliDSN) meilisearch.ServiceManager {
- serviceName := viper.GetString("search.service_name")
-
- otelTransport := otelhttp.NewTransport(
- http.DefaultTransport,
- otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
- return fmt.Sprintf("%s %s", serviceName, r.Method)
- }),
- )
-
- httpClient := &http.Client{
- Transport: otelTransport,
- }
-
- return meilisearch.New(dsn.Host,
- meilisearch.WithAPIKey(dsn.ApiKey),
- meilisearch.WithCustomClient(httpClient),
- meilisearch.WithContentEncoding(
- meilisearch.GzipEncoding,
- meilisearch.BestCompression,
- ),
- )
-}
diff --git a/data/drivers/postgres_test.go b/data/drivers/postgres_test.go
new file mode 100644
index 0000000..850314b
--- /dev/null
+++ b/data/drivers/postgres_test.go
@@ -0,0 +1,31 @@
+package drivers
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSplitHostPortWithPort(t *testing.T) {
+ host, port := SplitHostPort("localhost:5432")
+ assert.Equal(t, "localhost", host)
+ assert.Equal(t, "5432", port)
+}
+
+func TestSplitHostPortWithoutPort(t *testing.T) {
+ host, port := SplitHostPort("localhost")
+ assert.Equal(t, "localhost", host)
+ assert.Equal(t, "5432", port)
+}
+
+func TestSplitHostPortIPWithPort(t *testing.T) {
+ host, port := SplitHostPort("127.0.0.1:9999")
+ assert.Equal(t, "127.0.0.1", host)
+ assert.Equal(t, "9999", port)
+}
+
+func TestSplitHostPortEmpty(t *testing.T) {
+ host, port := SplitHostPort("")
+ assert.Equal(t, "", host)
+ assert.Equal(t, "5432", port)
+}
diff --git a/data/event.go b/data/event.go
index 37b8e6b..f7d6038 100644
--- a/data/event.go
+++ b/data/event.go
@@ -4,32 +4,137 @@ import (
"context"
"time"
- "github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
- "github.com/meilisearch/meilisearch-go"
"gorm.io/gorm"
)
type Event struct {
- Id uint `json:"id" gorm:"primarykey;autoincrement"`
- UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
- EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
- Name string `json:"name" gorm:"type:varchar(255);index;not null"`
- Type string `json:"type" gotm:"type:varchar(255);index;not null"`
- Description string `json:"description" gorm:"type:text;not null"`
- StartTime time.Time `json:"start_time" gorm:"index"`
- EndTime time.Time `json:"end_time" gorm:"index"`
- Owner uuid.UUID `json:"owner" gorm:"type:uuid;index;not null"`
- EnableKYC bool `json:"enable_kyc" gorm:"not null"`
+ Id uint `json:"id" gorm:"primarykey;autoincrement"`
+ UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
+ EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
+ Name string `json:"name" gorm:"type:varchar(255);index;not null"`
+ Type string `json:"type" gorm:"type:varchar(255);index;not null"` // official | party
+ Subtitle string `json:"subtitle" gorm:"type:text;not null;default:an amazing event"`
+ Description string `json:"description" gorm:"type:text"` // base64 markdown
+ AttendanceGuide string `json:"attendance_guide" gorm:"type:text"` // base64 markdown
+ StartTime time.Time `json:"start_time" gorm:"index;not null"`
+ EndTime time.Time `json:"end_time" gorm:"index;not null"`
+ Thumbnail string `json:"thumbnail" gorm:"type:varchar(255)"`
+ Owner uuid.UUID `json:"owner" gorm:"type:uuid;index;not null"`
+ EnableKYC bool `json:"enable_kyc" gorm:"not null"`
+ IsAgendaPublished bool `json:"is_agenda_published" gorm:"not null;default:false"`
+ Quota int64 `json:"quota" gorm:"not null"`
+ Limit int64 `json:"limit" gorm:"not null"`
}
-type EventSearchDoc struct {
- EventId string `json:"event_id"`
- Name string `json:"name"`
- Type string `json:"type"`
- Description string `json:"description"`
- StartTime time.Time `json:"start_time"`
- EndTime time.Time `json:"end_time"`
+type EventIndexDoc struct {
+ EventId string `json:"event_id" validate:"required"`
+ Name string `json:"name" validate:"required"`
+ Type string `json:"type" validate:"required"`
+ Subtitle string `json:"subtitle" validate:"required"`
+ Description string `json:"description"`
+ StartTime time.Time `json:"start_time" validate:"required"`
+ EndTime time.Time `json:"end_time" validate:"required"`
+ Thumbnail string `json:"thumbnail"`
+ EnableKYC bool `json:"enable_kyc" validate:"required"`
+ IsAgendaPublished bool `json:"is_agenda_published"`
+ Owner string `json:"owner"`
+}
+
+type EventListOptions struct {
+ TypeFilter string
+ OwnerId *uuid.UUID
+ SortBy string
+ SortOrder string
+ Limit int64
+ Offset int64
+}
+
+type eventOpts struct {
+ Name *string
+ Type *string
+ Subtitle *string
+ Description *string
+ AttendanceGuide *string
+ StartTime *time.Time
+ EndTime *time.Time
+ Thumbnail *string
+ Owner *uuid.UUID
+ EnableKYC *bool
+ IsAgendaPublished *bool
+ Quota *int64
+ Limit *int64
+}
+
+type EventOption func(*eventOpts)
+
+func WithEventName(v string) EventOption { return func(o *eventOpts) { o.Name = &v } }
+func WithEventType(v string) EventOption { return func(o *eventOpts) { o.Type = &v } }
+func WithEventSubtitle(v string) EventOption { return func(o *eventOpts) { o.Subtitle = &v } }
+func WithEventDescription(v string) EventOption { return func(o *eventOpts) { o.Description = &v } }
+func WithAttendanceGuide(v string) EventOption { return func(o *eventOpts) { o.AttendanceGuide = &v } }
+func WithEventStartTime(v time.Time) EventOption { return func(o *eventOpts) { o.StartTime = &v } }
+func WithEventEndTime(v time.Time) EventOption { return func(o *eventOpts) { o.EndTime = &v } }
+func WithThumbnail(v string) EventOption { return func(o *eventOpts) { o.Thumbnail = &v } }
+func WithOwner(v uuid.UUID) EventOption { return func(o *eventOpts) { o.Owner = &v } }
+func WithEnableKYC(v bool) EventOption { return func(o *eventOpts) { o.EnableKYC = &v } }
+func WithIsAgendaPublished(v bool) EventOption {
+ return func(o *eventOpts) { o.IsAgendaPublished = &v }
+}
+func WithQuota(v int64) EventOption { return func(o *eventOpts) { o.Quota = &v } }
+func WithLimit(v int64) EventOption { return func(o *eventOpts) { o.Limit = &v } }
+
+func applyEventOpts(opts []EventOption) *eventOpts {
+ o := &eventOpts{}
+ for _, opt := range opts {
+ opt(o)
+ }
+ return o
+}
+
+func NewEvent(opts ...EventOption) *Event {
+ o := applyEventOpts(opts)
+ e := &Event{}
+ if o.Name != nil {
+ e.Name = *o.Name
+ }
+ if o.Type != nil {
+ e.Type = *o.Type
+ }
+ if o.Subtitle != nil {
+ e.Subtitle = *o.Subtitle
+ }
+ if o.Description != nil {
+ e.Description = *o.Description
+ }
+ if o.AttendanceGuide != nil {
+ e.AttendanceGuide = *o.AttendanceGuide
+ }
+ if o.StartTime != nil {
+ e.StartTime = *o.StartTime
+ }
+ if o.EndTime != nil {
+ e.EndTime = *o.EndTime
+ }
+ if o.Thumbnail != nil {
+ e.Thumbnail = *o.Thumbnail
+ }
+ if o.Owner != nil {
+ e.Owner = *o.Owner
+ }
+ if o.EnableKYC != nil {
+ e.EnableKYC = *o.EnableKYC
+ }
+ if o.IsAgendaPublished != nil {
+ e.IsAgendaPublished = *o.IsAgendaPublished
+ }
+ if o.Quota != nil {
+ e.Quota = *o.Quota
+ }
+ if o.Limit != nil {
+ e.Limit = *o.Limit
+ }
+ return e
}
func (self *Event) GetEventById(ctx context.Context, eventId uuid.UUID) (*Event, error) {
@@ -49,110 +154,178 @@ func (self *Event) GetEventById(ctx context.Context, eventId uuid.UUID) (*Event,
return &event, nil
}
-func (self *Event) UpdateEventById(ctx context.Context, eventId uuid.UUID) error {
- // DB transaction
- if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- // Update by business key
- if err := tx.
- Model(&Event{}).
- Where("event_id = ?", eventId).
- Updates(self).Error; err != nil {
- return err
- }
-
- // Reload to ensure struct is fresh
- return tx.
- Where("event_id = ?", eventId).
- First(self).Error
- }); err != nil {
- return err
- }
-
- // Sync search index
- if err := self.UpdateSearchIndex(ctx); err != nil {
- return err
- }
-
- return nil
-}
-
func (self *Event) Create(ctx context.Context) error {
self.UUID = uuid.New()
self.EventId = uuid.New()
- // DB transaction only
if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Create(self).Error; err != nil {
- return err
- }
- return nil
+ return tx.Create(self).Error
}); err != nil {
return err
}
- // Search index (eventual consistency)
- if err := self.UpdateSearchIndex(ctx); err != nil {
- // TODO: async retry / log
- return err
- }
-
return nil
}
-func (self *Event) GetFullTable(ctx context.Context) (*[]Event, error) {
- var events []Event
- err := Database.WithContext(ctx).Find(&events).Error
- if err != nil {
- return nil, err
+func (self *Event) PatchByEventId(ctx context.Context, eventId uuid.UUID, opts ...EventOption) error {
+ o := applyEventOpts(opts)
+ updates := make(map[string]any)
+
+ if o.Name != nil {
+ updates["name"] = *o.Name
+ }
+ if o.Type != nil {
+ updates["type"] = *o.Type
+ }
+ if o.Subtitle != nil {
+ updates["subtitle"] = *o.Subtitle
+ }
+ if o.Description != nil {
+ updates["description"] = *o.Description
+ }
+ if o.AttendanceGuide != nil {
+ updates["attendance_guide"] = *o.AttendanceGuide
+ }
+ if o.StartTime != nil {
+ updates["start_time"] = *o.StartTime
+ }
+ if o.EndTime != nil {
+ updates["end_time"] = *o.EndTime
+ }
+ if o.Thumbnail != nil {
+ updates["thumbnail"] = *o.Thumbnail
+ }
+ if o.Owner != nil {
+ updates["owner"] = *o.Owner
+ }
+ if o.EnableKYC != nil {
+ updates["enable_kyc"] = *o.EnableKYC
+ }
+ if o.IsAgendaPublished != nil {
+ updates["is_agenda_published"] = *o.IsAgendaPublished
+ }
+ if o.Quota != nil {
+ updates["quota"] = *o.Quota
+ }
+ if o.Limit != nil {
+ updates["limit"] = *o.Limit
}
- return &events, err
-}
-func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventSearchDoc, error) {
- index := MeiliSearch.Index("event")
+ if len(updates) == 0 {
+ return nil
+ }
- // Fast read from MeiliSearch (no DB involved)
- result, err := index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
- Limit: limit,
- Offset: offset,
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ if err := tx.Model(&Event{}).
+ Where("event_id = ?", eventId).
+ Updates(updates).Error; err != nil {
+ return err
+ }
+ return tx.Where("event_id = ?", eventId).First(self).Error
})
+}
+
+func (self *Event) DeleteEventById(ctx context.Context, eventId uuid.UUID) error {
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ result := tx.Where("event_id = ?", eventId).Delete(&Event{})
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 {
+ return gorm.ErrRecordNotFound
+ }
+ return nil
+ })
+}
+
+func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventIndexDoc, error) {
+ var results []EventIndexDoc
+
+ err := Database.WithContext(ctx).
+ Model(&Event{}).
+ Select("event_id", "name", "type", "subtitle", "description", "start_time", "end_time", "thumbnail", "enable_kyc", "is_agenda_published", "owner").
+ Limit(int(limit)).
+ Offset(int(offset)).
+ Scan(&results).Error
+
if err != nil {
return nil, err
}
- var list []EventSearchDoc
- if err := mapstructure.Decode(result.Hits, &list); err != nil {
+ return &results, nil
+}
+
+func (self *Event) ListEventsWithOptions(ctx context.Context, opts EventListOptions) (*[]EventIndexDoc, int64, error) {
+ var results []EventIndexDoc
+ var total int64
+
+ selectCols := "event_id, name, type, subtitle, description, start_time, end_time, thumbnail, enable_kyc, is_agenda_published, owner"
+
+ baseQuery := Database.WithContext(ctx).Model(&Event{})
+
+ if opts.TypeFilter != "" {
+ baseQuery = baseQuery.Where("type = ?", opts.TypeFilter)
+ }
+ if opts.OwnerId != nil {
+ baseQuery = baseQuery.Where("owner = ?", opts.OwnerId)
+ }
+
+ if err := baseQuery.Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+
+ sortField := "start_time"
+ switch opts.SortBy {
+ case "end_time", "name":
+ sortField = opts.SortBy
+ }
+
+ sortOrder := "DESC"
+ if opts.SortOrder == "asc" {
+ sortOrder = "ASC"
+ }
+
+ err := baseQuery.
+ Select(selectCols).
+ Order(sortField + " " + sortOrder).
+ Limit(int(opts.Limit)).
+ Offset(int(opts.Offset)).
+ Scan(&results).Error
+
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return &results, total, nil
+}
+
+func (self *Event) GetEventsByUserId(ctx context.Context, userId uuid.UUID, limit, offset int64) (*[]EventIndexDoc, error) {
+ var results []EventIndexDoc
+
+ err := Database.WithContext(ctx).
+ Table("events").
+ Select(`
+ events.event_id,
+ events.name,
+ events.type,
+ events.subtitle,
+ events.description,
+ events.start_time,
+ events.end_time,
+ events.thumbnail,
+ events.enable_kyc,
+ (SELECT COUNT(*) FROM attendances WHERE attendances.event_id = events.event_id) as join_count
+ `).
+ Joins("JOIN attendances ON attendances.event_id = events.event_id").
+ Where("attendances.user_id = ?", userId).
+ Order("events.start_time DESC").
+ Limit(int(limit)).
+ Offset(int(offset)).
+ Scan(&results).Error
+
+ if err != nil {
return nil, err
}
- return &list, nil
-}
-
-func (self *Event) UpdateSearchIndex(ctx context.Context) error {
- doc := EventSearchDoc{
- EventId: self.EventId.String(),
- Name: self.Name,
- Type: self.Type,
- Description: self.Description,
- StartTime: self.StartTime,
- EndTime: self.EndTime,
- }
- index := MeiliSearch.Index("event")
-
- primaryKey := "event_id"
- opts := &meilisearch.DocumentOptions{
- PrimaryKey: &primaryKey,
- }
-
- if _, err := index.UpdateDocumentsWithContext(ctx, []EventSearchDoc{doc}, opts); err != nil {
- return err
- }
-
- return nil
-}
-
-func (self *Event) DeleteSearchIndex(ctx context.Context) error {
- index := MeiliSearch.Index("event")
- _, err := index.DeleteDocumentWithContext(ctx, self.EventId.String(), nil)
- return err
+ return &results, nil
}
diff --git a/data/event_test.go b/data/event_test.go
new file mode 100644
index 0000000..11ad55f
--- /dev/null
+++ b/data/event_test.go
@@ -0,0 +1,205 @@
+package data_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func makeEvent(owner uuid.UUID) *data.Event {
+ now := time.Now()
+ return data.NewEvent(
+ data.WithEventName("Test Event "+uuid.New().String()[:8]),
+ data.WithEventType("party"),
+ data.WithEventSubtitle("A test event"),
+ data.WithEventStartTime(now.Add(24*time.Hour)),
+ data.WithEventEndTime(now.Add(48*time.Hour)),
+ data.WithOwner(owner),
+ data.WithQuota(100),
+ data.WithLimit(50),
+ )
+}
+
+func TestEventCreate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+ assert.NotEqual(t, uuid.Nil, ev.EventId)
+}
+
+func TestEventGetById(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ got, err := new(data.Event).GetEventById(ctx, ev.EventId)
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ assert.Equal(t, ev.EventId, got.EventId)
+ assert.Equal(t, ev.Name, got.Name)
+}
+
+func TestEventGetByIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ got, err := new(data.Event).GetEventById(ctx, uuid.New())
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestEventDeleteById(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ require.NoError(t, new(data.Event).DeleteEventById(ctx, ev.EventId))
+
+ got, err := new(data.Event).GetEventById(ctx, ev.EventId)
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestEventDeleteByIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ err := new(data.Event).DeleteEventById(ctx, uuid.New())
+ require.Error(t, err)
+}
+
+func TestEventPatch(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ require.NoError(t, ev.PatchByEventId(ctx, ev.EventId,
+ data.WithEventName("Updated Name"),
+ data.WithEventSubtitle("Updated subtitle"),
+ ))
+
+ got, err := new(data.Event).GetEventById(ctx, ev.EventId)
+ require.NoError(t, err)
+ assert.Equal(t, "Updated Name", got.Name)
+ assert.Equal(t, "Updated subtitle", got.Subtitle)
+}
+
+func TestEventListWithOptions(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ for _, typ := range []string{"party", "party", "official"} {
+ ev := makeEvent(owner)
+ ev.Type = typ
+ require.NoError(t, ev.Create(ctx))
+ }
+
+ results, total, err := new(data.Event).ListEventsWithOptions(ctx, data.EventListOptions{
+ TypeFilter: "party",
+ Limit: 10,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), total)
+ assert.Len(t, *results, 2)
+}
+
+func TestEventListWithOwnerFilter(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner1 := uuid.New()
+ owner2 := uuid.New()
+ for i := 0; i < 3; i++ {
+ require.NoError(t, makeEvent(owner1).Create(ctx))
+ }
+ require.NoError(t, makeEvent(owner2).Create(ctx))
+
+ results, total, err := new(data.Event).ListEventsWithOptions(ctx, data.EventListOptions{
+ OwnerId: &owner1,
+ Limit: 10,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), total)
+ assert.Len(t, *results, 3)
+}
+
+func TestEventFastList(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ for i := 0; i < 5; i++ {
+ require.NoError(t, makeEvent(owner).Create(ctx))
+ }
+
+ limit, offset := int64(3), int64(0)
+ results, err := new(data.Event).FastListEvents(ctx, limit, offset)
+ require.NoError(t, err)
+ assert.Len(t, *results, 3)
+}
+
+func TestEventGetEventsByUserId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev1 := makeEvent(owner)
+ require.NoError(t, ev1.Create(ctx))
+ ev2 := makeEvent(owner)
+ require.NoError(t, ev2.Create(ctx))
+ ev3 := makeEvent(owner)
+ require.NoError(t, ev3.Create(ctx))
+
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+
+ for _, evId := range []uuid.UUID{ev1.EventId, ev2.EventId} {
+ a := data.NewAttendance(
+ data.WithEventId(evId),
+ data.WithUserId(u.UserId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := a.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ results, err := new(data.Event).GetEventsByUserId(ctx, u.UserId, 10, 0)
+ require.NoError(t, err)
+ require.NotNil(t, results)
+ assert.Len(t, *results, 2)
+}
+
+func TestEventGetEventsByUserIdEmpty(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ results, err := new(data.Event).GetEventsByUserId(ctx, uuid.New(), 10, 0)
+ require.NoError(t, err)
+ require.NotNil(t, results)
+ assert.Empty(t, *results)
+}
diff --git a/data/kyc.go b/data/kyc.go
new file mode 100644
index 0000000..223293c
--- /dev/null
+++ b/data/kyc.go
@@ -0,0 +1,130 @@
+package data
+
+import (
+ "context"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type Kyc struct {
+ Id uint `json:"id" gorm:"primarykey;autoincrement"`
+ UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"`
+ UserId uuid.UUID `json:"user_id" gorm:"type:uuid;not null"`
+ KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueindex;not null"`
+ Type string `json:"type" gorm:"type:varchar(255);not null"`
+ KycInfo string `json:"kyc_info" gorm:"type:text"` // aes256(base64)
+}
+
+type kycOpts struct {
+ UserId *uuid.UUID
+ Type *string
+ KycInfo *string
+}
+
+// KycOption configures a kycOpts value.
+type KycOption func(*kycOpts)
+
+func WithKycUserId(v uuid.UUID) KycOption { return func(o *kycOpts) { o.UserId = &v } }
+func WithKycType(v string) KycOption { return func(o *kycOpts) { o.Type = &v } }
+func WithKycInfo(v string) KycOption { return func(o *kycOpts) { o.KycInfo = &v } }
+
+func applyKycOpts(opts []KycOption) *kycOpts {
+ o := &kycOpts{}
+ for _, opt := range opts {
+ opt(o)
+ }
+ return o
+}
+
+func NewKyc(opts ...KycOption) *Kyc {
+ o := applyKycOpts(opts)
+ k := &Kyc{}
+ if o.UserId != nil {
+ k.UserId = *o.UserId
+ }
+ if o.Type != nil {
+ k.Type = *o.Type
+ }
+ if o.KycInfo != nil {
+ k.KycInfo = *o.KycInfo
+ }
+ return k
+}
+
+func (self *Kyc) Create(ctx context.Context) (uuid.UUID, error) {
+ self.UUID = uuid.New()
+ self.KycId = uuid.New()
+
+ err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ return tx.Create(self).Error
+ })
+
+ if err != nil {
+ return uuid.Nil, err
+ }
+
+ return self.KycId, nil
+}
+
+func (self *Kyc) GetByKycId(ctx context.Context, kycId *uuid.UUID) (*Kyc, error) {
+ var kyc Kyc
+ err := Database.WithContext(ctx).
+ Where("kyc_id = ?", kycId).
+ First(&kyc).Error
+
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &kyc, nil
+}
+
+func (self *Kyc) ListByUserId(ctx context.Context, userId *uuid.UUID) ([]Kyc, error) {
+ var list []Kyc
+ err := Database.WithContext(ctx).
+ Where("user_id = ?", userId).
+ Find(&list).Error
+
+ return list, err
+}
+
+func (self *Kyc) PatchByKycId(ctx context.Context, kycId *uuid.UUID, opts ...KycOption) error {
+ o := applyKycOpts(opts)
+ updates := make(map[string]any)
+
+ if o.UserId != nil {
+ updates["user_id"] = *o.UserId
+ }
+ if o.Type != nil {
+ updates["type"] = *o.Type
+ }
+ if o.KycInfo != nil {
+ updates["kyc_info"] = *o.KycInfo
+ }
+
+ if len(updates) == 0 {
+ return nil
+ }
+
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ return tx.Model(&Kyc{}).
+ Where("kyc_id = ?", kycId).
+ Updates(updates).Error
+ })
+}
+
+func (self *Kyc) DeleteByKycID(ctx context.Context, kycId *uuid.UUID) error {
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ return tx.Where("kyc_id = ?", kycId).
+ Delete(&Kyc{}).Error
+ })
+}
+
+func (self *Kyc) DeleteAllByUserId(ctx context.Context, userId *uuid.UUID) error {
+ return Database.WithContext(ctx).
+ Where("user_id = ?", userId).
+ Delete(&Kyc{}).Error
+}
diff --git a/data/kyc_test.go b/data/kyc_test.go
new file mode 100644
index 0000000..9a17e0c
--- /dev/null
+++ b/data/kyc_test.go
@@ -0,0 +1,118 @@
+package data_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func TestKycCreate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ userId := uuid.New()
+ k := data.NewKyc(
+ data.WithKycUserId(userId),
+ data.WithKycType("cnrid"),
+ data.WithKycInfo("encrypted-info"),
+ )
+ kycId, err := k.Create(ctx)
+ require.NoError(t, err)
+ assert.NotEqual(t, uuid.Nil, kycId)
+}
+
+func TestKycGetByKycId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ userId := uuid.New()
+ k := data.NewKyc(data.WithKycUserId(userId), data.WithKycType("cnrid"), data.WithKycInfo("info"))
+ kycId, err := k.Create(ctx)
+ require.NoError(t, err)
+
+ got, err := new(data.Kyc).GetByKycId(ctx, &kycId)
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ assert.Equal(t, userId, got.UserId)
+}
+
+func TestKycGetByKycIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ missing := uuid.New()
+ got, err := new(data.Kyc).GetByKycId(ctx, &missing)
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestKycListByUserId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ userId := uuid.New()
+ for _, typ := range []string{"cnrid", "passport"} {
+ k := data.NewKyc(data.WithKycUserId(userId), data.WithKycType(typ), data.WithKycInfo("info"))
+ _, err := k.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ list, err := new(data.Kyc).ListByUserId(ctx, &userId)
+ require.NoError(t, err)
+ assert.Len(t, list, 2)
+}
+
+func TestKycPatchByKycId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ userId := uuid.New()
+ k := data.NewKyc(data.WithKycUserId(userId), data.WithKycType("cnrid"), data.WithKycInfo("original"))
+ kycId, err := k.Create(ctx)
+ require.NoError(t, err)
+
+ require.NoError(t, new(data.Kyc).PatchByKycId(ctx, &kycId, data.WithKycInfo("updated")))
+
+ got, err := new(data.Kyc).GetByKycId(ctx, &kycId)
+ require.NoError(t, err)
+ assert.Equal(t, "updated", got.KycInfo)
+}
+
+func TestKycDeleteByKycID(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ userId := uuid.New()
+ k := data.NewKyc(data.WithKycUserId(userId), data.WithKycType("cnrid"), data.WithKycInfo("info"))
+ kycId, err := k.Create(ctx)
+ require.NoError(t, err)
+
+ require.NoError(t, new(data.Kyc).DeleteByKycID(ctx, &kycId))
+
+ got, err := new(data.Kyc).GetByKycId(ctx, &kycId)
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestKycDeleteAllByUserId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ userId := uuid.New()
+ for i := 0; i < 3; i++ {
+ k := data.NewKyc(data.WithKycUserId(userId), data.WithKycType("cnrid"), data.WithKycInfo("info"))
+ _, err := k.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, new(data.Kyc).DeleteAllByUserId(ctx, &userId))
+
+ list, err := new(data.Kyc).ListByUserId(ctx, &userId)
+ require.NoError(t, err)
+ assert.Empty(t, list)
+}
diff --git a/data/stats.go b/data/stats.go
new file mode 100644
index 0000000..830806a
--- /dev/null
+++ b/data/stats.go
@@ -0,0 +1,70 @@
+package data
+
+import (
+ "context"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type PermissionLevelCount struct {
+ PermissionLevel uint `json:"permission_level"`
+ Count int64 `json:"count"`
+}
+
+type EventStatDoc struct {
+ EventId uuid.UUID `json:"event_id"`
+ Name string `json:"name"`
+ JoinCount int64 `json:"join_count"`
+ CheckinCount int64 `json:"checkin_count"`
+}
+
+type GlobalStats struct{}
+
+func (self *GlobalStats) TotalUsers(ctx context.Context) (int64, error) {
+ var count int64
+ err := Database.WithContext(ctx).Model(&User{}).Count(&count).Error
+ return count, err
+}
+
+func (self *GlobalStats) UsersPerPermissionLevel(ctx context.Context) (*[]PermissionLevelCount, error) {
+ var results []PermissionLevelCount
+
+ err := Database.WithContext(ctx).
+ Model(&User{}).
+ Select("permission_level, COUNT(*) as count").
+ Group("permission_level").
+ Order("permission_level ASC").
+ Scan(&results).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &results, nil
+}
+
+func (self *GlobalStats) EventJoinCheckinCounts(ctx context.Context) (*[]EventStatDoc, error) {
+ var results []EventStatDoc
+
+ zero := time.Time{}
+
+ err := Database.WithContext(ctx).
+ Table("events").
+ Select(`
+ events.event_id,
+ events.name,
+ COUNT(attendances.id) AS join_count,
+ SUM(CASE WHEN attendances.checkin_at > ? THEN 1 ELSE 0 END) AS checkin_count
+ `, zero).
+ Joins("LEFT JOIN attendances ON attendances.event_id = events.event_id").
+ Group("events.id, events.event_id, events.name").
+ Order("events.id ASC").
+ Scan(&results).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &results, nil
+}
diff --git a/data/stats_test.go b/data/stats_test.go
new file mode 100644
index 0000000..98a8755
--- /dev/null
+++ b/data/stats_test.go
@@ -0,0 +1,146 @@
+package data_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func TestGlobalStatsTotalUsers(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for i := 0; i < 3; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ }
+
+ count, err := new(data.GlobalStats).TotalUsers(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), count)
+}
+
+func TestGlobalStatsUsersPerPermissionLevel(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for _, lvl := range []uint{10, 10, 30} {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(lvl),
+ )
+ require.NoError(t, u.Create(ctx))
+ }
+
+ results, err := new(data.GlobalStats).UsersPerPermissionLevel(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, results)
+
+ countMap := make(map[uint]int64)
+ for _, r := range *results {
+ countMap[r.PermissionLevel] = r.Count
+ }
+ assert.Equal(t, int64(2), countMap[10])
+ assert.Equal(t, int64(1), countMap[30])
+}
+
+func TestGlobalStatsEventJoinCheckinCounts(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev := makeEvent(owner)
+ require.NoError(t, ev.Create(ctx))
+
+ for i, checkin := range []bool{true, false} {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+
+ a := data.NewAttendance(
+ data.WithEventId(ev.EventId),
+ data.WithUserId(u.UserId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ if checkin {
+ a.CheckinAt = time.Now()
+ }
+ _ = i
+ _, err := a.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ results, err := new(data.GlobalStats).EventJoinCheckinCounts(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, results)
+ require.Len(t, *results, 1)
+
+ stat := (*results)[0]
+ assert.Equal(t, ev.EventId, stat.EventId)
+ assert.Equal(t, int64(2), stat.JoinCount)
+ assert.Equal(t, int64(1), stat.CheckinCount)
+}
+
+func TestGlobalStatsEventJoinCheckinCountsEmpty(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ results, err := new(data.GlobalStats).EventJoinCheckinCounts(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, results)
+ assert.Empty(t, *results)
+}
+
+func TestGlobalStatsEventJoinCheckinMultipleEvents(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := uuid.New()
+ ev1 := makeEvent(owner)
+ require.NoError(t, ev1.Create(ctx))
+ ev2 := makeEvent(owner)
+ require.NoError(t, ev2.Create(ctx))
+
+ for _, evId := range []uuid.UUID{ev1.EventId, ev2.EventId} {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@test.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ a := data.NewAttendance(
+ data.WithEventId(evId),
+ data.WithUserId(u.UserId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := a.Create(ctx)
+ require.NoError(t, err)
+ }
+
+ results, err := new(data.GlobalStats).EventJoinCheckinCounts(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, results)
+ assert.Len(t, *results, 2)
+ for _, stat := range *results {
+ assert.Equal(t, int64(1), stat.JoinCount)
+ assert.Equal(t, int64(0), stat.CheckinCount)
+ }
+}
diff --git a/data/user.go b/data/user.go
index 0494e47..8a16e3c 100644
--- a/data/user.go
+++ b/data/user.go
@@ -3,9 +3,7 @@ package data
import (
"context"
- "github.com/go-viper/mapstructure/v2"
"github.com/google/uuid"
- "github.com/meilisearch/meilisearch-go"
"gorm.io/gorm"
)
@@ -21,56 +19,94 @@ type User struct {
Bio string `json:"bio" gorm:"type:text"`
PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"`
AllowPublic bool `json:"allow_public" gorm:"default:false;not null"`
+ KycInfo string `json:"kyc_info" gorm:"type:text"`
}
-type UserSearchDoc struct {
- UserId string `json:"user_id"`
- Email string `json:"email"`
- Username string `json:"username"`
- Type string `json:"type"`
+type UserIndexDoc struct {
+ UserId string `json:"user_id" validate:"required"`
+ Email string `json:"email" validate:"required"`
+ Username string `json:"username" validate:"required"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
}
-func (self *User) SetEmail(s string) *User {
- self.Email = s
- return self
+type UserAdminDoc struct {
+ UserId string `json:"user_id"`
+ Email string `json:"email"`
+ Username string `json:"username"`
+ Nickname string `json:"nickname"`
+ Subtitle string `json:"subtitle"`
+ Avatar string `json:"avatar"`
+ PermissionLevel uint `json:"permission_level"`
}
-func (self *User) SetUsername(s string) *User {
- self.Username = s
- return self
+type UserListOptions struct {
+ PermissionLevel *uint
+ SortBy string // "id" | "permission_level"
+ SortOrder string // "asc" | "desc"
+ Limit int
+ Offset int
}
-func (self *User) SetNickname(s string) *User {
- self.Nickname = s
- return self
+type userOpts struct {
+ Email *string
+ Username *string
+ Nickname *string
+ Subtitle *string
+ Avatar *string
+ Bio *string
+ PermissionLevel *uint
+ AllowPublic *bool
}
-func (self *User) SetSubtitle(s string) *User {
- self.Subtitle = s
- return self
+type UserOption func(*userOpts)
+
+func WithEmail(v string) UserOption { return func(o *userOpts) { o.Email = &v } }
+func WithUsername(v string) UserOption { return func(o *userOpts) { o.Username = &v } }
+func WithNickname(v string) UserOption { return func(o *userOpts) { o.Nickname = &v } }
+func WithSubtitle(v string) UserOption { return func(o *userOpts) { o.Subtitle = &v } }
+func WithAvatar(v string) UserOption { return func(o *userOpts) { o.Avatar = &v } }
+func WithBio(v string) UserOption { return func(o *userOpts) { o.Bio = &v } }
+func WithPermissionLevel(v uint) UserOption { return func(o *userOpts) { o.PermissionLevel = &v } }
+func WithAllowPublic(v bool) UserOption { return func(o *userOpts) { o.AllowPublic = &v } }
+
+func applyUserOpts(opts []UserOption) *userOpts {
+ o := &userOpts{}
+ for _, opt := range opts {
+ opt(o)
+ }
+ return o
}
-func (self *User) SetAvatar(s string) *User {
- self.Avatar = s
- return self
-}
-
-func (self *User) SetBio(s string) *User {
- self.Bio = s
- return self
-}
-
-func (self *User) SetPermissionLevel(s uint) *User {
- self.PermissionLevel = s
- return self
-}
-
-func (self *User) SetAllowPublic(s bool) *User {
- self.AllowPublic = s
- return self
+func NewUser(opts ...UserOption) *User {
+ o := applyUserOpts(opts)
+ u := &User{}
+ if o.Email != nil {
+ u.Email = *o.Email
+ }
+ if o.Username != nil {
+ u.Username = *o.Username
+ }
+ if o.Nickname != nil {
+ u.Nickname = *o.Nickname
+ }
+ if o.Subtitle != nil {
+ u.Subtitle = *o.Subtitle
+ }
+ if o.Avatar != nil {
+ u.Avatar = *o.Avatar
+ }
+ if o.Bio != nil {
+ u.Bio = *o.Bio
+ }
+ if o.PermissionLevel != nil {
+ u.PermissionLevel = *o.PermissionLevel
+ }
+ if o.AllowPublic != nil {
+ u.AllowPublic = *o.AllowPublic
+ }
+ return u
}
func (self *User) GetByEmail(ctx context.Context, email *string) (*User, error) {
@@ -81,9 +117,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
}
@@ -111,92 +144,106 @@ func (self *User) Create(ctx context.Context) error {
self.UUID = uuid.New()
self.UserId = uuid.New()
- // DB transaction only
if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- if err := tx.Create(self).Error; err != nil {
- return err
- }
- return nil
+ return tx.Create(self).Error
}); err != nil {
return err
}
- // Search index (eventual consistency)
- if err := self.UpdateSearchIndex(&ctx); err != nil {
- // TODO: async retry / log
- return err
- }
-
return nil
}
-func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID) 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 {
- return err
- }
+func (self *User) PatchByUserId(ctx context.Context, userId uuid.UUID, opts ...UserOption) error {
+ o := applyUserOpts(opts)
+ updates := make(map[string]any)
+
+ if o.Email != nil {
+ updates["email"] = *o.Email
+ }
+ if o.Username != nil {
+ updates["username"] = *o.Username
+ }
+ if o.Nickname != nil {
+ updates["nickname"] = *o.Nickname
+ }
+ if o.Subtitle != nil {
+ updates["subtitle"] = *o.Subtitle
+ }
+ if o.Avatar != nil {
+ updates["avatar"] = *o.Avatar
+ }
+ if o.Bio != nil {
+ updates["bio"] = *o.Bio
+ }
+ if o.PermissionLevel != nil {
+ updates["permission_level"] = *o.PermissionLevel
+ }
+ if o.AllowPublic != nil {
+ updates["allow_public"] = *o.AllowPublic
+ }
+
+ if len(updates) == 0 {
return nil
- })
-}
-
-func (self *User) GetFullTable(ctx context.Context) (*[]User, error) {
- var users []User
- err := Database.WithContext(ctx).Find(&users).Error
- if err != nil {
- return nil, err
}
- return &users, nil
+
+ return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ return tx.Model(&User{}).
+ Where("user_id = ?", userId).
+ Updates(updates).Error
+ })
}
-func (self *User) FastListUsers(ctx context.Context, limit, offset *int64) (*[]UserSearchDoc, error) {
- index := MeiliSearch.Index("user")
+func (self *User) FastListUsers(ctx context.Context, limit, offset *int) (*[]UserIndexDoc, error) {
+ var results []UserIndexDoc
+
+ query := Database.WithContext(ctx).Model(&User{})
+
+ err := query.Select("user_id", "email", "username", "nickname", "subtitle", "avatar").
+ Limit(*limit).
+ Offset(*offset).
+ Scan(&results).Error
- // Fast read from MeiliSearch, no DB involved
- result, err := index.SearchWithContext(ctx, "", &meilisearch.SearchRequest{
- Limit: *limit,
- Offset: *offset,
- })
if err != nil {
return nil, err
}
- var list []UserSearchDoc
- if err := mapstructure.Decode(result.Hits, &list); err != nil {
- return nil, err
- }
-
- return &list, nil
+ return &results, nil
}
-func (self *User) UpdateSearchIndex(ctx *context.Context) error {
- doc := UserSearchDoc{
- UserId: self.UserId.String(),
- Email: self.Email,
- Username: self.Username,
- Nickname: self.Nickname,
- Subtitle: self.Subtitle,
- Avatar: self.Avatar,
- }
- index := MeiliSearch.Index("user")
+func (self *User) ListUsersFiltered(ctx context.Context, opts UserListOptions) (*[]UserAdminDoc, int64, error) {
+ var results []UserAdminDoc
+ var total int64
- primaryKey := "user_id"
- opts := &meilisearch.DocumentOptions{
- PrimaryKey: &primaryKey,
+ base := Database.WithContext(ctx).Model(&User{})
+
+ if opts.PermissionLevel != nil {
+ base = base.Where("permission_level = ?", *opts.PermissionLevel)
}
- if _, err := index.UpdateDocumentsWithContext(
- *ctx,
- []UserSearchDoc{doc},
- opts,
- ); err != nil {
- return err
+ if err := base.Count(&total).Error; err != nil {
+ return nil, 0, err
}
- return nil
-}
-
-func (self *User) DeleteSearchIndex(ctx *context.Context) error {
- index := MeiliSearch.Index("user")
- _, err := index.DeleteDocumentWithContext(*ctx, self.UserId.String(), nil)
- return err
+ sortField := "id"
+ if opts.SortBy == "permission_level" {
+ sortField = "permission_level"
+ }
+
+ sortOrder := "ASC"
+ if opts.SortOrder == "desc" {
+ sortOrder = "DESC"
+ }
+
+ err := base.
+ Select("user_id", "email", "username", "nickname", "subtitle", "avatar", "permission_level").
+ Order(sortField + " " + sortOrder).
+ Limit(opts.Limit).
+ Offset(opts.Offset).
+ Scan(&results).Error
+
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return &results, total, nil
}
diff --git a/data/user_test.go b/data/user_test.go
new file mode 100644
index 0000000..a556cd2
--- /dev/null
+++ b/data/user_test.go
@@ -0,0 +1,162 @@
+package data_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func TestUserCreateAndGetByEmail(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := data.NewUser(
+ data.WithEmail("alice@example.com"),
+ data.WithUsername("alice"),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ assert.NotEqual(t, uuid.Nil, u.UserId)
+
+ got, err := new(data.User).GetByEmail(ctx, &u.Email)
+ require.NoError(t, err)
+ assert.Equal(t, u.Email, got.Email)
+ assert.Equal(t, u.UserId, got.UserId)
+}
+
+func TestUserGetByEmailNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ email := "nobody@example.com"
+ _, err := new(data.User).GetByEmail(ctx, &email)
+ require.Error(t, err)
+}
+
+func TestUserGetByUserId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := data.NewUser(
+ data.WithEmail("bob@example.com"),
+ data.WithUsername("bob"),
+ data.WithPermissionLevel(20),
+ )
+ require.NoError(t, u.Create(ctx))
+
+ got, err := new(data.User).GetByUserId(ctx, &u.UserId)
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ assert.Equal(t, u.Email, got.Email)
+}
+
+func TestUserGetByUserIdNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ missing := uuid.New()
+ got, err := new(data.User).GetByUserId(ctx, &missing)
+ require.NoError(t, err)
+ assert.Nil(t, got)
+}
+
+func TestUserPatchByUserId(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := data.NewUser(data.WithEmail("carol@example.com"), data.WithUsername("carol"), data.WithPermissionLevel(10))
+ require.NoError(t, u.Create(ctx))
+
+ require.NoError(t, new(data.User).PatchByUserId(ctx, u.UserId,
+ data.WithNickname("Carol Smith"),
+ data.WithPermissionLevel(20),
+ ))
+
+ got, err := new(data.User).GetByUserId(ctx, &u.UserId)
+ require.NoError(t, err)
+ assert.Equal(t, "Carol Smith", got.Nickname)
+ assert.Equal(t, uint(20), got.PermissionLevel)
+}
+
+func TestUserPatchByUserIdNoOp(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := data.NewUser(data.WithEmail("dave@example.com"), data.WithUsername("dave"), data.WithPermissionLevel(10))
+ require.NoError(t, u.Create(ctx))
+
+ require.NoError(t, new(data.User).PatchByUserId(ctx, u.UserId))
+}
+
+func TestUserFastListUsers(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for i := 0; i < 5; i++ {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@example.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(10),
+ )
+ require.NoError(t, u.Create(ctx))
+ }
+
+ limit, offset := 3, 0
+ results, err := new(data.User).FastListUsers(ctx, &limit, &offset)
+ require.NoError(t, err)
+ assert.Len(t, *results, 3)
+}
+
+func TestUserListUsersFiltered(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for _, lvl := range []uint{10, 10, 30} {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@example.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(lvl),
+ )
+ require.NoError(t, u.Create(ctx))
+ }
+
+ lvl := uint(10)
+ results, total, err := new(data.User).ListUsersFiltered(ctx, data.UserListOptions{
+ PermissionLevel: &lvl,
+ Limit: 10,
+ Offset: 0,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), total)
+ assert.Len(t, *results, 2)
+}
+
+func TestUserListUsersSortAndPaginate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for _, lvl := range []uint{10, 20, 30} {
+ u := data.NewUser(
+ data.WithEmail(uuid.New().String()+"@example.com"),
+ data.WithUsername(uuid.New().String()),
+ data.WithPermissionLevel(lvl),
+ )
+ require.NoError(t, u.Create(ctx))
+ }
+
+ results, total, err := new(data.User).ListUsersFiltered(ctx, data.UserListOptions{
+ SortBy: "permission_level",
+ SortOrder: "desc",
+ Limit: 2,
+ Offset: 0,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), total)
+ assert.Len(t, *results, 2)
+ assert.GreaterOrEqual(t, (*results)[0].PermissionLevel, (*results)[1].PermissionLevel)
+}
diff --git a/deploy/Caddyfile b/deploy/Caddyfile
new file mode 100644
index 0000000..b52ce12
--- /dev/null
+++ b/deploy/Caddyfile
@@ -0,0 +1,11 @@
+test.nix.org.cn {
+ tls /etc/caddy/cert.crt /etc/caddy/cert.key
+
+ handle /app/api/* {
+ reverse_proxy cms-server:8000
+ }
+
+ handle /app/* {
+ reverse_proxy cms-client:3000
+ }
+}
diff --git a/deploy/compose.yaml b/deploy/compose.yaml
new file mode 100644
index 0000000..51e658d
--- /dev/null
+++ b/deploy/compose.yaml
@@ -0,0 +1,83 @@
+services:
+ postgres:
+ image: docker.io/postgres:18-alpine
+ container_name: postgres
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ 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
+ timeout: 5s
+ retries: 5
+
+ redis:
+ image: docker.io/redis:8-alpine
+ container_name: redis
+ volumes:
+ - ./data/redis:/data
+ healthcheck:
+ test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
+ interval: 5s
+ timeout: 3s
+ retries: 5
+
+ lgtm:
+ image: grafana/otel-lgtm:latest
+ container_name: lgtm
+ 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
+ restart: always
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./Caddyfile:/etc/caddy/Caddyfile
+ - ./cert.crt:/etc/caddy/cert.crt
+ - ./cert.key:/etc/caddy/cert.key
+ - ./data/caddy/data:/data
+ - ./data/caddy/config:/config
+
+ cms-client:
+ image: registry.asnk.io/nixcn/cms-client:dev
+ container_name: cms-client
+ restart: always
+ depends_on:
+ lgtm:
+ condition: service_healthy
+
+ cms-server:
+ image: registry.asnk.io/nixcn/cms-server:dev
+ container_name: cms-server
+ restart: always
+ volumes:
+ - ./config.yaml:/app/config.yaml:ro
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ lgtm:
+ condition: service_healthy
diff --git a/devenv.lock b/devenv.lock
index e963bf7..1a8e41f 100644
--- a/devenv.lock
+++ b/devenv.lock
@@ -3,10 +3,11 @@
"devenv": {
"locked": {
"dir": "src/modules",
- "lastModified": 1766087669,
+ "lastModified": 1773273745,
+ "narHash": "sha256-poUn0001nXGKTxZpy+RhtO7Il/sKmJCmuxeyVqFNTbg=",
"owner": "cachix",
"repo": "devenv",
- "rev": "c03eed645ea94da7afbee29da76436b7ce33a5cb",
+ "rev": "2e5cfe21801beb8323d60689d9a82b5adc847988",
"type": "github"
},
"original": {
@@ -19,25 +20,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1765121682,
+ "lastModified": 1761588595,
+ "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
- "type": "github"
- },
- "original": {
- "owner": "edolstra",
- "repo": "flake-compat",
- "type": "github"
- }
- },
- "flake-compat_2": {
- "flake": false,
- "locked": {
- "lastModified": 1765121682,
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
+ "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"type": "github"
},
"original": {
@@ -51,10 +38,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1731533236,
+ "lastModified": 1701680307,
+ "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -67,37 +55,17 @@
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
- "nixpkgs": [
- "nixpkgs"
- ]
- },
- "locked": {
- "lastModified": 1765911976,
- "owner": "cachix",
- "repo": "git-hooks.nix",
- "rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
- "type": "github"
- },
- "original": {
- "owner": "cachix",
- "repo": "git-hooks.nix",
- "type": "github"
- }
- },
- "git-hooks_2": {
- "inputs": {
- "flake-compat": "flake-compat_2",
- "gitignore": "gitignore_2",
"nixpkgs": [
"go-overlay",
"nixpkgs"
]
},
"locked": {
- "lastModified": 1765911976,
+ "lastModified": 1765016596,
+ "narHash": "sha256-rhSqPNxDVow7OQKi4qS5H8Au0P4S3AYbawBSmJNUtBQ=",
"owner": "cachix",
"repo": "git-hooks.nix",
- "rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
+ "rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c",
"type": "github"
},
"original": {
@@ -107,26 +75,6 @@
}
},
"gitignore": {
- "inputs": {
- "nixpkgs": [
- "git-hooks",
- "nixpkgs"
- ]
- },
- "locked": {
- "lastModified": 1762808025,
- "owner": "hercules-ci",
- "repo": "gitignore.nix",
- "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
- "type": "github"
- },
- "original": {
- "owner": "hercules-ci",
- "repo": "gitignore.nix",
- "type": "github"
- }
- },
- "gitignore_2": {
"inputs": {
"nixpkgs": [
"go-overlay",
@@ -135,10 +83,11 @@
]
},
"locked": {
- "lastModified": 1762808025,
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
- "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
@@ -150,16 +99,17 @@
"go-overlay": {
"inputs": {
"flake-utils": "flake-utils",
- "git-hooks": "git-hooks_2",
+ "git-hooks": "git-hooks",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
- "lastModified": 1766126609,
+ "lastModified": 1773206113,
+ "narHash": "sha256-2fS3hM8TS3uG2ndBesQcZbB8BwFPKK0SCqmmB1qrk3s=",
"owner": "purpleclay",
"repo": "go-overlay",
- "rev": "959f32b00fd3d462d4d570bd118b4be03c3f2019",
+ "rev": "53d3f4039617f8c3d8b0c31b1a7716f3cebd1afa",
"type": "github"
},
"original": {
@@ -169,11 +119,15 @@
}
},
"nixpkgs": {
+ "inputs": {
+ "nixpkgs-src": "nixpkgs-src"
+ },
"locked": {
- "lastModified": 1764580874,
+ "lastModified": 1772749504,
+ "narHash": "sha256-eqtQIz0alxkQPym+Zh/33gdDjkkch9o6eHnMPnXFXN0=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
- "rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d",
+ "rev": "08543693199362c1fddb8f52126030d0d374ba2e",
"type": "github"
},
"original": {
@@ -183,20 +137,34 @@
"type": "github"
}
},
+ "nixpkgs-src": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1772173633,
+ "narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
"devenv": "devenv",
- "git-hooks": "git-hooks",
"go-overlay": "go-overlay",
- "nixpkgs": "nixpkgs",
- "pre-commit-hooks": [
- "git-hooks"
- ]
+ "nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
@@ -211,4 +179,4 @@
},
"root": "root",
"version": 7
-}
+}
\ No newline at end of file
diff --git a/devenv.nix b/devenv.nix
index f1aff93..75a0c67 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -1,10 +1,6 @@
{ pkgs, ... }:
{
- process.managers.process-compose = {
- settings.log_level = "info";
- };
-
packages = with pkgs; [
git
just
@@ -24,20 +20,14 @@
languages = {
go = {
enable = true;
- version = "1.25.5";
+ version = "1.26.0";
};
- javascript.enable = true;
- javascript.corepack.enable = true;
};
env.PODMAN_COMPOSE_PROVIDER = "none";
processes = {
- client-cms = {
- exec = "pnpm run dev";
- cwd = "./client/cms";
- };
- backend.exec = "sleep 30 && just watch-back";
+ server.exec = "sleep 30 && just watch";
lgtm.exec = ''
podman rm -f lgtm || true
podman run --name lgtm \
@@ -50,11 +40,13 @@
services = {
redis = {
enable = true;
+ port = 6379;
};
postgres = {
enable = true;
createDatabase = true;
- listen_addresses = "127.0.0.1";
+ listen_addresses = "0.0.0.0";
+ port = 5432;
initialDatabases = [
{
name = "postgres";
@@ -63,8 +55,5 @@
}
];
};
- meilisearch = {
- enable = true;
- };
};
}
diff --git a/devenv.yaml b/devenv.yaml
index 8030b4c..25beeaf 100644
--- a/devenv.yaml
+++ b/devenv.yaml
@@ -1,8 +1,9 @@
inputs:
- go-overlay:
- url: github:purpleclay/go-overlay
- inputs:
- nixpkgs:
- follows: nixpkgs
- nixpkgs:
- url: github:cachix/devenv-nixpkgs/rolling
+ go-overlay:
+ url: github:purpleclay/go-overlay
+ inputs:
+ nixpkgs:
+ follows: nixpkgs
+ nixpkgs:
+ url: github:cachix/devenv-nixpkgs/rolling
+strictPorts: true
diff --git a/docs/docs.go b/docs/docs.go
new file mode 100644
index 0000000..b678d48
--- /dev/null
+++ b/docs/docs.go
@@ -0,0 +1,4526 @@
+// 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}}",
+ "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}}",
+ "basePath": "{{.BasePath}}",
+ "paths": {
+ "/agenda/list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns all agendas for the specified event, regardless of status. Manager only.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "List All Agendas",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event ID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_agenda.AgendaListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/my-list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns the calling user's agenda submissions for the specified event. User must be a joined attendee (Lv10+).",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "My Agenda List",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event ID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.Agenda"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not an Attendee",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/review": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Manager sets the status of an agenda to approved or rejected. Not allowed after agenda is published.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Review Agenda",
+ "parameters": [
+ {
+ "description": "Review Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.AgendaReviewData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Already Published",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event or Agenda Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/schedule": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns all approved and scheduled agenda items, sorted by start_time ascending. Returns 403 if the agenda has not been published.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Get Agenda Schedule",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event ID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.AgendaDoc"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Agenda Not Published",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Manager sets start_time and end_time on an approved agenda item. Available even after publish.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Schedule Agenda",
+ "parameters": [
+ {
+ "description": "Schedule Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.AgendaScheduleData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Not Approved",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Agenda Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/submit": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Creates a new agenda item for a specific attendance record.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Submit Agenda",
+ "parameters": [
+ {
+ "description": "Agenda Submission Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.SubmitData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_agenda.SubmitResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/update": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Submitter may edit their own pending agendas before the event deadline. Managers may edit any agenda with no restrictions.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Update Agenda",
+ "parameters": [
+ {
+ "description": "Agenda Update Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.AgendaUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Not Pending / Deadline Passed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Submitter",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Agenda Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/auth/exchange": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successful exchange",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.ExchangeResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Successful request",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.MagicResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Turnstile Verification Failed",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": [
+ "application/json",
+ "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": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Invalid or Expired Verification Code",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Successful rotation",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.TokenResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Invalid Refresh Token",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Successful token issuance",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.TokenResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Invalid or Expired Code",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/attendance": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Retrieves the paginated list of attendees with optional filters. Only accessible by the event owner (Manager). Supports name substring search and KYC status filtering.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Get Attendance List",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event UUID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Substring filter on attendee nickname",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "KYC filter: 'with_kyc' or 'without_kyc'",
+ "name": "kyc_status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Maximum number of results to return (default 20)",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Number of results to skip (default 0)",
+ "name": "offset",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort field: 'checkin_at' (default) or 'id'",
+ "name": "sort_by",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort direction: 'asc' or 'desc' (default)",
+ "name": "sort_order",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_event.AttendanceListResponse"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Event Owner",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/checkin": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successfully generated code",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.CheckinResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Current attendance status",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.CheckinQueryResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/checkin/submit": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Attendance marked successfully",
+ "schema": {
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/create": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Allows a Lv30+ user to create a new event. Users at exactly Lv30 may only create events with type 'party'. Sets type and enable_kyc, which are immutable after creation.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Create an Event",
+ "parameters": [
+ {
+ "description": "Event Creation Details",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventCreateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully created the event",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventCreateResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Permission Denied / Type Not Allowed for this level",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error / Database Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/delete": {
+ "delete": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Permanently deletes an event. Requires Lv40+.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Delete an Event",
+ "parameters": [
+ {
+ "description": "Event to delete",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventDeleteData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully deleted",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/guide": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Fetching attendance guide of an event using its UUID.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Get Event Guide",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event UUID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.AttendanceGuideResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/info": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successful retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventInfoResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/join": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Join an Event",
+ "parameters": [
+ {
+ "description": "Event Join Details (UserId and EventId are required)",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventJoinData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully joined the event",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventJoinResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input or UUID Parse Failed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized / Missing User ID / Event Limit Exceeded",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error / Database Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns a paginated list of events. Supports filtering by type and sorting. Lv30 users are automatically scoped to events they own.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "List Events",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "Maximum number of events to return (default 20)",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Number of events to skip",
+ "name": "offset",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Filter by event type: 'official' or 'party'",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort field: 'start_time' (default), 'end_time', 'name'",
+ "name": "sort_by",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort direction: 'asc' or 'desc' (default)",
+ "name": "sort_order",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful paginated list retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventListResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/stats": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns join count, checkin count, KYC pass rate, and agenda submission count. Only accessible by the event owner (Manager).",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Get Event Statistics",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event UUID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Statistics retrieved successfully",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventStatsResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Event Owner",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/update": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Allows the event owner (Manager) to update name, subtitle, description, start_time, end_time, thumbnail, and is_agenda_published. Changes to type or enable_kyc are rejected. is_agenda_published is write-once: it can only be set to true (requires at least one agenda submission) and cannot be reverted.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Update an Event",
+ "parameters": [
+ {
+ "description": "Fields to update (all optional except event_id)",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully updated",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Immutable Field / Agenda Pre-flight Failed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Event Owner",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/kyc/query": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Checks the current state of a KYC session and updates local database if approved.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "KYC"
+ ],
+ "summary": "Query KYC Status",
+ "parameters": [
+ {
+ "description": "KYC query data (KycId)",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_kyc.KycQueryData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Query processed (success/pending/failed)",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_kyc.KycQueryResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid UUID or input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/kyc/session": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "KYC"
+ ],
+ "summary": "Create KYC Session",
+ "parameters": [
+ {
+ "description": "KYC session data (Type and Base64 Identity)",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_kyc.KycSessionData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Session created successfully",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_kyc.KycSessionResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid input or decode failed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Missing User ID",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error / KYC Service Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/stats/global": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns total users, user counts per permission_level, and per-event join/checkin counts.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Stats"
+ ],
+ "summary": "Global Stats",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_stats.GlobalStatsResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/info": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successful profile retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_user.UserInfoData"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "User Not Found",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error (UUID Parse Failed)",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/info/{user_id}": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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 Other User Information",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Other user id",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful profile retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_user.UserInfoData"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "User Not Public",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "User Not Found",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error (UUID Parse Failed)",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns a paginated list of users with permission_level included. Supports filtering by permission_level and sorting.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "User"
+ ],
+ "summary": "List Users (Admin)",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Maximum number of users to return (default 20)",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Number of users to skip",
+ "name": "offset",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Sort field: 'id' (default) | 'permission_level'",
+ "name": "sort_by",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort direction: 'asc' (default) | 'desc'",
+ "name": "sort_order",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Filter by exact permission level",
+ "name": "permission_level",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful paginated list retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_user.UserListResponse"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/update": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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.UserInfoUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful profile update",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/update/{user_id}": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Lv40+ operators may update any user with a strictly lower permission_level. Editable fields: all profile fields plus permission_level (new value must be below operator's own level).",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "User"
+ ],
+ "summary": "Admin Update User",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Target User ID",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Fields to update",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_user.UserInfoUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Permission Matrix Violation",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Target User Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "data.Agenda": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "attendance_id": {
+ "type": "string"
+ },
+ "description": {
+ "description": "base64 encoded markdown",
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "status": {
+ "description": "pending | approved | rejected",
+ "type": "string"
+ },
+ "uuid": {
+ "type": "string"
+ }
+ }
+ },
+ "data.AgendaDoc": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "attendance_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ }
+ },
+ "data.EventStatDoc": {
+ "type": "object",
+ "properties": {
+ "checkin_count": {
+ "type": "integer"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "data.PermissionLevelCount": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": "integer"
+ },
+ "permission_level": {
+ "type": "integer"
+ }
+ }
+ },
+ "service_agenda.AgendaListItem": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "attendance_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "user_profile": {
+ "$ref": "#/definitions/service_agenda.AgendaUserProfile"
+ }
+ }
+ },
+ "service_agenda.AgendaReviewData": {
+ "type": "object",
+ "required": [
+ "agenda_id",
+ "event_id",
+ "status"
+ ],
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "status": {
+ "description": "approved | rejected",
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.AgendaScheduleData": {
+ "type": "object",
+ "required": [
+ "agenda_id",
+ "end_time",
+ "start_time"
+ ],
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.AgendaUpdateData": {
+ "type": "object",
+ "required": [
+ "agenda_id"
+ ],
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.AgendaUserProfile": {
+ "type": "object",
+ "properties": {
+ "nickname": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.SubmitData": {
+ "type": "object",
+ "required": [
+ "description",
+ "event_id",
+ "name"
+ ],
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.SubmitResponse": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.ExchangeData": {
+ "type": "object",
+ "required": [
+ "client_id",
+ "redirect_uri"
+ ],
+ "properties": {
+ "client_id": {
+ "type": "string"
+ },
+ "redirect_uri": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.ExchangeResponse": {
+ "type": "object",
+ "required": [
+ "redirect_uri"
+ ],
+ "properties": {
+ "redirect_uri": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.MagicData": {
+ "type": "object",
+ "required": [
+ "client_id",
+ "email",
+ "redirect_uri",
+ "turnstile_token"
+ ],
+ "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.MagicResponse": {
+ "type": "object",
+ "required": [
+ "uri"
+ ],
+ "properties": {
+ "uri": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.RefreshData": {
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "properties": {
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.TokenData": {
+ "type": "object",
+ "required": [
+ "code"
+ ],
+ "properties": {
+ "code": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.TokenResponse": {
+ "type": "object",
+ "required": [
+ "access_token",
+ "refresh_token"
+ ],
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.AttendanceGuideResponse": {
+ "type": "object",
+ "required": [
+ "attendance_guide"
+ ],
+ "properties": {
+ "attendance_guide": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.AttendanceListResponse": {
+ "type": "object",
+ "required": [
+ "attendance_id",
+ "user_info"
+ ],
+ "properties": {
+ "attendance_id": {
+ "type": "string"
+ },
+ "checked_in_at": {
+ "type": "string"
+ },
+ "joined_at": {
+ "type": "string"
+ },
+ "kyc_info": {},
+ "kyc_status": {
+ "type": "string"
+ },
+ "kyc_type": {
+ "type": "string"
+ },
+ "user_info": {
+ "$ref": "#/definitions/service_user.UserInfoData"
+ }
+ }
+ },
+ "service_event.CheckinQueryResponse": {
+ "type": "object",
+ "properties": {
+ "checkin_at": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.CheckinResponse": {
+ "type": "object",
+ "required": [
+ "checkin_code"
+ ],
+ "properties": {
+ "checkin_code": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.CheckinSubmitData": {
+ "type": "object",
+ "required": [
+ "checkin_code"
+ ],
+ "properties": {
+ "checkin_code": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventCreateData": {
+ "type": "object",
+ "required": [
+ "name",
+ "type"
+ ],
+ "properties": {
+ "attendance_guide": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "enable_kyc": {
+ "type": "boolean"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "quota": {
+ "type": "integer"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventCreateResponse": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "event_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventDeleteData": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "event_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventInfoResponse": {
+ "type": "object",
+ "required": [
+ "enable_kyc",
+ "end_time",
+ "event_id",
+ "name",
+ "start_time",
+ "subtitle",
+ "type"
+ ],
+ "properties": {
+ "checkin_count": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "enable_kyc": {
+ "type": "boolean"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "is_agenda_published": {
+ "type": "boolean"
+ },
+ "is_checked_in": {
+ "type": "boolean"
+ },
+ "is_joined": {
+ "type": "boolean"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "owner": {
+ "type": "string"
+ },
+ "quota": {
+ "type": "integer"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventJoinData": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "event_id": {
+ "type": "string"
+ },
+ "kyc_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventJoinResponse": {
+ "type": "object",
+ "required": [
+ "attendance_id"
+ ],
+ "properties": {
+ "attendance_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventListItems": {
+ "type": "object",
+ "required": [
+ "enable_kyc",
+ "end_time",
+ "event_id",
+ "name",
+ "start_time",
+ "subtitle",
+ "type"
+ ],
+ "properties": {
+ "checkin_count": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "enable_kyc": {
+ "type": "boolean"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "is_agenda_published": {
+ "type": "boolean"
+ },
+ "is_checked_in": {
+ "type": "boolean"
+ },
+ "is_joined": {
+ "type": "boolean"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "owner": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventListResponse": {
+ "type": "object",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_event.EventListItems"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "service_event.EventStatsResponse": {
+ "type": "object",
+ "properties": {
+ "agenda_submission_count": {
+ "type": "integer"
+ },
+ "checkin_count": {
+ "type": "integer"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "kyc_pass_rate": {
+ "type": "number"
+ }
+ }
+ },
+ "service_event.EventUpdateData": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "attendance_guide": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "is_agenda_published": {
+ "type": "boolean"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "quota": {
+ "type": "integer"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycQueryData": {
+ "type": "object",
+ "required": [
+ "kyc_id"
+ ],
+ "properties": {
+ "kyc_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycQueryResponse": {
+ "type": "object",
+ "required": [
+ "status"
+ ],
+ "properties": {
+ "status": {
+ "description": "success | pending | failed",
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycSessionData": {
+ "type": "object",
+ "required": [
+ "identity",
+ "type"
+ ],
+ "properties": {
+ "identity": {
+ "description": "base64 json",
+ "type": "string"
+ },
+ "type": {
+ "description": "cnrid | passport",
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycSessionResponse": {
+ "type": "object",
+ "required": [
+ "status"
+ ],
+ "properties": {
+ "kyc_id": {
+ "type": "string"
+ },
+ "redirect_uri": {
+ "type": "string"
+ },
+ "status": {
+ "description": "success | processing",
+ "type": "string"
+ }
+ }
+ },
+ "service_stats.GlobalStatsResponse": {
+ "type": "object",
+ "properties": {
+ "event_join_checkin": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.EventStatDoc"
+ }
+ },
+ "total_users": {
+ "type": "integer"
+ },
+ "users_per_level": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.PermissionLevelCount"
+ }
+ }
+ }
+ },
+ "service_user.UserInfoData": {
+ "type": "object",
+ "required": [
+ "allow_public",
+ "email",
+ "permission_level",
+ "user_id",
+ "username"
+ ],
+ "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.UserInfoUpdateData": {
+ "type": "object",
+ "properties": {
+ "allow_public": {
+ "type": "boolean"
+ },
+ "avatar": {
+ "type": "string"
+ },
+ "bio": {
+ "type": "string"
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "permission_level": {
+ "type": "integer"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "service_user.UserListResponse": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "permission_level": {
+ "type": "integer"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "utils.RespStatus": {
+ "type": "object",
+ "required": [
+ "code",
+ "data",
+ "error_id",
+ "status"
+ ],
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {},
+ "error_id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "Bearer": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ }
+ }
+}`
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = &swag.Spec{
+ Version: "1.0",
+ Host: "localhost:8000",
+ BasePath: "/app/api/v1",
+ Schemes: []string{"http", "https"},
+ Title: "NixCN CMS API",
+ Description: "API Docs based on Gin framework",
+ 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..f7a018d
--- /dev/null
+++ b/docs/swagger.json
@@ -0,0 +1,4506 @@
+{
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "swagger": "2.0",
+ "info": {
+ "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": "/app/api/v1",
+ "paths": {
+ "/agenda/list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns all agendas for the specified event, regardless of status. Manager only.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "List All Agendas",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event ID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_agenda.AgendaListItem"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/my-list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns the calling user's agenda submissions for the specified event. User must be a joined attendee (Lv10+).",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "My Agenda List",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event ID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.Agenda"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not an Attendee",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/review": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Manager sets the status of an agenda to approved or rejected. Not allowed after agenda is published.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Review Agenda",
+ "parameters": [
+ {
+ "description": "Review Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.AgendaReviewData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Already Published",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event or Agenda Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/schedule": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns all approved and scheduled agenda items, sorted by start_time ascending. Returns 403 if the agenda has not been published.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Get Agenda Schedule",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event ID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.AgendaDoc"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Agenda Not Published",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Manager sets start_time and end_time on an approved agenda item. Available even after publish.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Schedule Agenda",
+ "parameters": [
+ {
+ "description": "Schedule Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.AgendaScheduleData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Not Approved",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Agenda Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/submit": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Creates a new agenda item for a specific attendance record.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Submit Agenda",
+ "parameters": [
+ {
+ "description": "Agenda Submission Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.SubmitData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_agenda.SubmitResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/agenda/update": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Submitter may edit their own pending agendas before the event deadline. Managers may edit any agenda with no restrictions.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Agenda"
+ ],
+ "summary": "Update Agenda",
+ "parameters": [
+ {
+ "description": "Agenda Update Data",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_agenda.AgendaUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Not Pending / Deadline Passed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Submitter",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Agenda Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/auth/exchange": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successful exchange",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.ExchangeResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Successful request",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.MagicResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Turnstile Verification Failed",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": [
+ "application/json",
+ "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": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Invalid or Expired Verification Code",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Successful rotation",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.TokenResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Invalid Refresh Token",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Successful token issuance",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_auth.TokenResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Invalid or Expired Code",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/attendance": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Retrieves the paginated list of attendees with optional filters. Only accessible by the event owner (Manager). Supports name substring search and KYC status filtering.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Get Attendance List",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event UUID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Substring filter on attendee nickname",
+ "name": "name",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "KYC filter: 'with_kyc' or 'without_kyc'",
+ "name": "kyc_status",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Maximum number of results to return (default 20)",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Number of results to skip (default 0)",
+ "name": "offset",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort field: 'checkin_at' (default) or 'id'",
+ "name": "sort_by",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort direction: 'asc' or 'desc' (default)",
+ "name": "sort_order",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_event.AttendanceListResponse"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Event Owner",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/checkin": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successfully generated code",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.CheckinResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/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": "Current attendance status",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.CheckinQueryResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/checkin/submit": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Attendance marked successfully",
+ "schema": {
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/create": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Allows a Lv30+ user to create a new event. Users at exactly Lv30 may only create events with type 'party'. Sets type and enable_kyc, which are immutable after creation.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Create an Event",
+ "parameters": [
+ {
+ "description": "Event Creation Details",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventCreateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully created the event",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventCreateResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Permission Denied / Type Not Allowed for this level",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error / Database Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/delete": {
+ "delete": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Permanently deletes an event. Requires Lv40+.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Delete an Event",
+ "parameters": [
+ {
+ "description": "Event to delete",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventDeleteData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully deleted",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/guide": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Fetching attendance guide of an event using its UUID.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Get Event Guide",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event UUID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.AttendanceGuideResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/info": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successful retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventInfoResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/join": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Join an Event",
+ "parameters": [
+ {
+ "description": "Event Join Details (UserId and EventId are required)",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventJoinData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully joined the event",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventJoinResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input or UUID Parse Failed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized / Missing User ID / Event Limit Exceeded",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error / Database Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns a paginated list of events. Supports filtering by type and sorting. Lv30 users are automatically scoped to events they own.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "List Events",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "Maximum number of events to return (default 20)",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Number of events to skip",
+ "name": "offset",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Filter by event type: 'official' or 'party'",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort field: 'start_time' (default), 'end_time', 'name'",
+ "name": "sort_by",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort direction: 'asc' or 'desc' (default)",
+ "name": "sort_order",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful paginated list retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventListResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/stats": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns join count, checkin count, KYC pass rate, and agenda submission count. Only accessible by the event owner (Manager).",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Get Event Statistics",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Event UUID",
+ "name": "event_id",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Statistics retrieved successfully",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_event.EventStatsResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Event Owner",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/event/update": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Allows the event owner (Manager) to update name, subtitle, description, start_time, end_time, thumbnail, and is_agenda_published. Changes to type or enable_kyc are rejected. is_agenda_published is write-once: it can only be set to true (requires at least one agenda submission) and cannot be reverted.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Event"
+ ],
+ "summary": "Update an Event",
+ "parameters": [
+ {
+ "description": "Fields to update (all optional except event_id)",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_event.EventUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully updated",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input / Immutable Field / Agenda Pre-flight Failed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Not Event Owner",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Event Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/kyc/query": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Checks the current state of a KYC session and updates local database if approved.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "KYC"
+ ],
+ "summary": "Query KYC Status",
+ "parameters": [
+ {
+ "description": "KYC query data (KycId)",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_kyc.KycQueryData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Query processed (success/pending/failed)",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_kyc.KycQueryResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid UUID or input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/kyc/session": {
+ "post": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "KYC"
+ ],
+ "summary": "Create KYC Session",
+ "parameters": [
+ {
+ "description": "KYC session data (Type and Base64 Identity)",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_kyc.KycSessionData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Session created successfully",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_kyc.KycSessionResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid input or decode failed",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Missing User ID",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error / KYC Service Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/stats/global": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns total users, user counts per permission_level, and per-event join/checkin counts.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Stats"
+ ],
+ "summary": "Global Stats",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_stats.GlobalStatsResponse"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/info": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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": "Successful profile retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_user.UserInfoData"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "User Not Found",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error (UUID Parse Failed)",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/info/{user_id}": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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 Other User Information",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Other user id",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful profile retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service_user.UserInfoData"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Missing User ID / Unauthorized",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "User Not Public",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "User Not Found",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error (UUID Parse Failed)",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/list": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Returns a paginated list of users with permission_level included. Supports filtering by permission_level and sorting.",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "User"
+ ],
+ "summary": "List Users (Admin)",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Maximum number of users to return (default 20)",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Number of users to skip",
+ "name": "offset",
+ "in": "query",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "Sort field: 'id' (default) | 'permission_level'",
+ "name": "sort_by",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Sort direction: 'asc' (default) | 'desc'",
+ "name": "sort_order",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Filter by exact permission level",
+ "name": "permission_level",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful paginated list retrieval",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_user.UserListResponse"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/update": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "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.UserInfoUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful profile update",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "/user/update/{user_id}": {
+ "patch": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Lv40+ operators may update any user with a strictly lower permission_level. Editable fields: all profile fields plus permission_level (new value must be below operator's own level).",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "User"
+ ],
+ "summary": "Admin Update User",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Target User ID",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Fields to update",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/service_user.UserInfoUpdateData"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Invalid Input",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "403": {
+ "description": "Permission Matrix Violation",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/utils.RespStatus"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "404": {
+ "description": "Target User Not Found",
+ "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"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "data.Agenda": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "attendance_id": {
+ "type": "string"
+ },
+ "description": {
+ "description": "base64 encoded markdown",
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "status": {
+ "description": "pending | approved | rejected",
+ "type": "string"
+ },
+ "uuid": {
+ "type": "string"
+ }
+ }
+ },
+ "data.AgendaDoc": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "attendance_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ }
+ },
+ "data.EventStatDoc": {
+ "type": "object",
+ "properties": {
+ "checkin_count": {
+ "type": "integer"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "data.PermissionLevelCount": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": "integer"
+ },
+ "permission_level": {
+ "type": "integer"
+ }
+ }
+ },
+ "service_agenda.AgendaListItem": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "attendance_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "user_profile": {
+ "$ref": "#/definitions/service_agenda.AgendaUserProfile"
+ }
+ }
+ },
+ "service_agenda.AgendaReviewData": {
+ "type": "object",
+ "required": [
+ "agenda_id",
+ "event_id",
+ "status"
+ ],
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "status": {
+ "description": "approved | rejected",
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.AgendaScheduleData": {
+ "type": "object",
+ "required": [
+ "agenda_id",
+ "end_time",
+ "start_time"
+ ],
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.AgendaUpdateData": {
+ "type": "object",
+ "required": [
+ "agenda_id"
+ ],
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.AgendaUserProfile": {
+ "type": "object",
+ "properties": {
+ "nickname": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.SubmitData": {
+ "type": "object",
+ "required": [
+ "description",
+ "event_id",
+ "name"
+ ],
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "service_agenda.SubmitResponse": {
+ "type": "object",
+ "properties": {
+ "agenda_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.ExchangeData": {
+ "type": "object",
+ "required": [
+ "client_id",
+ "redirect_uri"
+ ],
+ "properties": {
+ "client_id": {
+ "type": "string"
+ },
+ "redirect_uri": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.ExchangeResponse": {
+ "type": "object",
+ "required": [
+ "redirect_uri"
+ ],
+ "properties": {
+ "redirect_uri": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.MagicData": {
+ "type": "object",
+ "required": [
+ "client_id",
+ "email",
+ "redirect_uri",
+ "turnstile_token"
+ ],
+ "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.MagicResponse": {
+ "type": "object",
+ "required": [
+ "uri"
+ ],
+ "properties": {
+ "uri": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.RefreshData": {
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "properties": {
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.TokenData": {
+ "type": "object",
+ "required": [
+ "code"
+ ],
+ "properties": {
+ "code": {
+ "type": "string"
+ }
+ }
+ },
+ "service_auth.TokenResponse": {
+ "type": "object",
+ "required": [
+ "access_token",
+ "refresh_token"
+ ],
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.AttendanceGuideResponse": {
+ "type": "object",
+ "required": [
+ "attendance_guide"
+ ],
+ "properties": {
+ "attendance_guide": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.AttendanceListResponse": {
+ "type": "object",
+ "required": [
+ "attendance_id",
+ "user_info"
+ ],
+ "properties": {
+ "attendance_id": {
+ "type": "string"
+ },
+ "checked_in_at": {
+ "type": "string"
+ },
+ "joined_at": {
+ "type": "string"
+ },
+ "kyc_info": {},
+ "kyc_status": {
+ "type": "string"
+ },
+ "kyc_type": {
+ "type": "string"
+ },
+ "user_info": {
+ "$ref": "#/definitions/service_user.UserInfoData"
+ }
+ }
+ },
+ "service_event.CheckinQueryResponse": {
+ "type": "object",
+ "properties": {
+ "checkin_at": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.CheckinResponse": {
+ "type": "object",
+ "required": [
+ "checkin_code"
+ ],
+ "properties": {
+ "checkin_code": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.CheckinSubmitData": {
+ "type": "object",
+ "required": [
+ "checkin_code"
+ ],
+ "properties": {
+ "checkin_code": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventCreateData": {
+ "type": "object",
+ "required": [
+ "name",
+ "type"
+ ],
+ "properties": {
+ "attendance_guide": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "enable_kyc": {
+ "type": "boolean"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "quota": {
+ "type": "integer"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventCreateResponse": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "event_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventDeleteData": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "event_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventInfoResponse": {
+ "type": "object",
+ "required": [
+ "enable_kyc",
+ "end_time",
+ "event_id",
+ "name",
+ "start_time",
+ "subtitle",
+ "type"
+ ],
+ "properties": {
+ "checkin_count": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "enable_kyc": {
+ "type": "boolean"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "is_agenda_published": {
+ "type": "boolean"
+ },
+ "is_checked_in": {
+ "type": "boolean"
+ },
+ "is_joined": {
+ "type": "boolean"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "owner": {
+ "type": "string"
+ },
+ "quota": {
+ "type": "integer"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventJoinData": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "event_id": {
+ "type": "string"
+ },
+ "kyc_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventJoinResponse": {
+ "type": "object",
+ "required": [
+ "attendance_id"
+ ],
+ "properties": {
+ "attendance_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventListItems": {
+ "type": "object",
+ "required": [
+ "enable_kyc",
+ "end_time",
+ "event_id",
+ "name",
+ "start_time",
+ "subtitle",
+ "type"
+ ],
+ "properties": {
+ "checkin_count": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "enable_kyc": {
+ "type": "boolean"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "is_agenda_published": {
+ "type": "boolean"
+ },
+ "is_checked_in": {
+ "type": "boolean"
+ },
+ "is_joined": {
+ "type": "boolean"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "owner": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ }
+ },
+ "service_event.EventListResponse": {
+ "type": "object",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/service_event.EventListItems"
+ }
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ },
+ "service_event.EventStatsResponse": {
+ "type": "object",
+ "properties": {
+ "agenda_submission_count": {
+ "type": "integer"
+ },
+ "checkin_count": {
+ "type": "integer"
+ },
+ "join_count": {
+ "type": "integer"
+ },
+ "kyc_pass_rate": {
+ "type": "number"
+ }
+ }
+ },
+ "service_event.EventUpdateData": {
+ "type": "object",
+ "required": [
+ "event_id"
+ ],
+ "properties": {
+ "attendance_guide": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "end_time": {
+ "type": "string"
+ },
+ "event_id": {
+ "type": "string"
+ },
+ "is_agenda_published": {
+ "type": "boolean"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "quota": {
+ "type": "integer"
+ },
+ "start_time": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "thumbnail": {
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycQueryData": {
+ "type": "object",
+ "required": [
+ "kyc_id"
+ ],
+ "properties": {
+ "kyc_id": {
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycQueryResponse": {
+ "type": "object",
+ "required": [
+ "status"
+ ],
+ "properties": {
+ "status": {
+ "description": "success | pending | failed",
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycSessionData": {
+ "type": "object",
+ "required": [
+ "identity",
+ "type"
+ ],
+ "properties": {
+ "identity": {
+ "description": "base64 json",
+ "type": "string"
+ },
+ "type": {
+ "description": "cnrid | passport",
+ "type": "string"
+ }
+ }
+ },
+ "service_kyc.KycSessionResponse": {
+ "type": "object",
+ "required": [
+ "status"
+ ],
+ "properties": {
+ "kyc_id": {
+ "type": "string"
+ },
+ "redirect_uri": {
+ "type": "string"
+ },
+ "status": {
+ "description": "success | processing",
+ "type": "string"
+ }
+ }
+ },
+ "service_stats.GlobalStatsResponse": {
+ "type": "object",
+ "properties": {
+ "event_join_checkin": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.EventStatDoc"
+ }
+ },
+ "total_users": {
+ "type": "integer"
+ },
+ "users_per_level": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/data.PermissionLevelCount"
+ }
+ }
+ }
+ },
+ "service_user.UserInfoData": {
+ "type": "object",
+ "required": [
+ "allow_public",
+ "email",
+ "permission_level",
+ "user_id",
+ "username"
+ ],
+ "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.UserInfoUpdateData": {
+ "type": "object",
+ "properties": {
+ "allow_public": {
+ "type": "boolean"
+ },
+ "avatar": {
+ "type": "string"
+ },
+ "bio": {
+ "type": "string"
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "permission_level": {
+ "type": "integer"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "service_user.UserListResponse": {
+ "type": "object",
+ "properties": {
+ "avatar": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "nickname": {
+ "type": "string"
+ },
+ "permission_level": {
+ "type": "integer"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "utils.RespStatus": {
+ "type": "object",
+ "required": [
+ "code",
+ "data",
+ "error_id",
+ "status"
+ ],
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {},
+ "error_id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "securityDefinitions": {
+ "Bearer": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
new file mode 100644
index 0000000..2a827df
--- /dev/null
+++ b/docs/swagger.yaml
@@ -0,0 +1,2573 @@
+basePath: /app/api/v1
+definitions:
+ data.Agenda:
+ properties:
+ agenda_id:
+ type: string
+ attendance_id:
+ type: string
+ description:
+ description: base64 encoded markdown
+ type: string
+ end_time:
+ type: string
+ id:
+ type: integer
+ name:
+ type: string
+ start_time:
+ type: string
+ status:
+ description: pending | approved | rejected
+ type: string
+ uuid:
+ type: string
+ type: object
+ data.AgendaDoc:
+ properties:
+ agenda_id:
+ type: string
+ attendance_id:
+ type: string
+ description:
+ type: string
+ end_time:
+ type: string
+ name:
+ type: string
+ start_time:
+ type: string
+ status:
+ type: string
+ type: object
+ data.EventStatDoc:
+ properties:
+ checkin_count:
+ type: integer
+ event_id:
+ type: string
+ join_count:
+ type: integer
+ name:
+ type: string
+ type: object
+ data.PermissionLevelCount:
+ properties:
+ count:
+ type: integer
+ permission_level:
+ type: integer
+ type: object
+ service_agenda.AgendaListItem:
+ properties:
+ agenda_id:
+ type: string
+ attendance_id:
+ type: string
+ description:
+ type: string
+ end_time:
+ type: string
+ name:
+ type: string
+ start_time:
+ type: string
+ status:
+ type: string
+ user_profile:
+ $ref: '#/definitions/service_agenda.AgendaUserProfile'
+ type: object
+ service_agenda.AgendaReviewData:
+ properties:
+ agenda_id:
+ type: string
+ event_id:
+ type: string
+ status:
+ description: approved | rejected
+ type: string
+ required:
+ - agenda_id
+ - event_id
+ - status
+ type: object
+ service_agenda.AgendaScheduleData:
+ properties:
+ agenda_id:
+ type: string
+ end_time:
+ type: string
+ start_time:
+ type: string
+ required:
+ - agenda_id
+ - end_time
+ - start_time
+ type: object
+ service_agenda.AgendaUpdateData:
+ properties:
+ agenda_id:
+ type: string
+ description:
+ type: string
+ name:
+ type: string
+ required:
+ - agenda_id
+ type: object
+ service_agenda.AgendaUserProfile:
+ properties:
+ nickname:
+ type: string
+ user_id:
+ type: string
+ username:
+ type: string
+ type: object
+ service_agenda.SubmitData:
+ properties:
+ description:
+ type: string
+ event_id:
+ type: string
+ name:
+ type: string
+ required:
+ - description
+ - event_id
+ - name
+ type: object
+ service_agenda.SubmitResponse:
+ properties:
+ agenda_id:
+ type: string
+ type: object
+ service_auth.ExchangeData:
+ properties:
+ client_id:
+ type: string
+ redirect_uri:
+ type: string
+ state:
+ type: string
+ required:
+ - client_id
+ - redirect_uri
+ type: object
+ service_auth.ExchangeResponse:
+ properties:
+ redirect_uri:
+ type: string
+ required:
+ - redirect_uri
+ 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
+ required:
+ - client_id
+ - email
+ - redirect_uri
+ - turnstile_token
+ type: object
+ service_auth.MagicResponse:
+ properties:
+ uri:
+ type: string
+ required:
+ - uri
+ type: object
+ service_auth.RefreshData:
+ properties:
+ refresh_token:
+ type: string
+ required:
+ - refresh_token
+ type: object
+ service_auth.TokenData:
+ properties:
+ code:
+ type: string
+ required:
+ - code
+ type: object
+ service_auth.TokenResponse:
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ required:
+ - access_token
+ - refresh_token
+ type: object
+ service_event.AttendanceGuideResponse:
+ properties:
+ attendance_guide:
+ type: string
+ required:
+ - attendance_guide
+ type: object
+ service_event.AttendanceListResponse:
+ properties:
+ attendance_id:
+ type: string
+ checked_in_at:
+ type: string
+ joined_at:
+ type: string
+ kyc_info: {}
+ kyc_status:
+ type: string
+ kyc_type:
+ type: string
+ user_info:
+ $ref: '#/definitions/service_user.UserInfoData'
+ required:
+ - attendance_id
+ - user_info
+ type: object
+ service_event.CheckinQueryResponse:
+ properties:
+ checkin_at:
+ type: string
+ type: object
+ service_event.CheckinResponse:
+ properties:
+ checkin_code:
+ type: string
+ required:
+ - checkin_code
+ type: object
+ service_event.CheckinSubmitData:
+ properties:
+ checkin_code:
+ type: string
+ required:
+ - checkin_code
+ type: object
+ service_event.EventCreateData:
+ properties:
+ attendance_guide:
+ type: string
+ description:
+ type: string
+ enable_kyc:
+ type: boolean
+ end_time:
+ type: string
+ limit:
+ type: integer
+ name:
+ type: string
+ quota:
+ type: integer
+ start_time:
+ type: string
+ subtitle:
+ type: string
+ thumbnail:
+ type: string
+ type:
+ type: string
+ required:
+ - name
+ - type
+ type: object
+ service_event.EventCreateResponse:
+ properties:
+ event_id:
+ type: string
+ required:
+ - event_id
+ type: object
+ service_event.EventDeleteData:
+ properties:
+ event_id:
+ type: string
+ required:
+ - event_id
+ type: object
+ service_event.EventInfoResponse:
+ properties:
+ checkin_count:
+ type: integer
+ description:
+ type: string
+ enable_kyc:
+ type: boolean
+ end_time:
+ type: string
+ event_id:
+ type: string
+ is_agenda_published:
+ type: boolean
+ is_checked_in:
+ type: boolean
+ is_joined:
+ type: boolean
+ join_count:
+ type: integer
+ limit:
+ type: integer
+ name:
+ type: string
+ owner:
+ type: string
+ quota:
+ type: integer
+ start_time:
+ type: string
+ subtitle:
+ type: string
+ thumbnail:
+ type: string
+ type:
+ type: string
+ required:
+ - enable_kyc
+ - end_time
+ - event_id
+ - name
+ - start_time
+ - subtitle
+ - type
+ type: object
+ service_event.EventJoinData:
+ properties:
+ event_id:
+ type: string
+ kyc_id:
+ type: string
+ required:
+ - event_id
+ type: object
+ service_event.EventJoinResponse:
+ properties:
+ attendance_id:
+ type: string
+ required:
+ - attendance_id
+ type: object
+ service_event.EventListItems:
+ properties:
+ checkin_count:
+ type: integer
+ description:
+ type: string
+ enable_kyc:
+ type: boolean
+ end_time:
+ type: string
+ event_id:
+ type: string
+ is_agenda_published:
+ type: boolean
+ is_checked_in:
+ type: boolean
+ is_joined:
+ type: boolean
+ join_count:
+ type: integer
+ name:
+ type: string
+ owner:
+ type: string
+ start_time:
+ type: string
+ subtitle:
+ type: string
+ thumbnail:
+ type: string
+ type:
+ type: string
+ required:
+ - enable_kyc
+ - end_time
+ - event_id
+ - name
+ - start_time
+ - subtitle
+ - type
+ type: object
+ service_event.EventListResponse:
+ properties:
+ items:
+ items:
+ $ref: '#/definitions/service_event.EventListItems'
+ type: array
+ total:
+ type: integer
+ type: object
+ service_event.EventStatsResponse:
+ properties:
+ agenda_submission_count:
+ type: integer
+ checkin_count:
+ type: integer
+ join_count:
+ type: integer
+ kyc_pass_rate:
+ type: number
+ type: object
+ service_event.EventUpdateData:
+ properties:
+ attendance_guide:
+ type: string
+ description:
+ type: string
+ end_time:
+ type: string
+ event_id:
+ type: string
+ is_agenda_published:
+ type: boolean
+ limit:
+ type: integer
+ name:
+ type: string
+ quota:
+ type: integer
+ start_time:
+ type: string
+ subtitle:
+ type: string
+ thumbnail:
+ type: string
+ required:
+ - event_id
+ type: object
+ service_kyc.KycQueryData:
+ properties:
+ kyc_id:
+ type: string
+ required:
+ - kyc_id
+ type: object
+ service_kyc.KycQueryResponse:
+ properties:
+ status:
+ description: success | pending | failed
+ type: string
+ required:
+ - status
+ type: object
+ service_kyc.KycSessionData:
+ properties:
+ identity:
+ description: base64 json
+ type: string
+ type:
+ description: cnrid | passport
+ type: string
+ required:
+ - identity
+ - type
+ type: object
+ service_kyc.KycSessionResponse:
+ properties:
+ kyc_id:
+ type: string
+ redirect_uri:
+ type: string
+ status:
+ description: success | processing
+ type: string
+ required:
+ - status
+ type: object
+ service_stats.GlobalStatsResponse:
+ properties:
+ event_join_checkin:
+ items:
+ $ref: '#/definitions/data.EventStatDoc'
+ type: array
+ total_users:
+ type: integer
+ users_per_level:
+ items:
+ $ref: '#/definitions/data.PermissionLevelCount'
+ type: array
+ 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
+ required:
+ - allow_public
+ - email
+ - permission_level
+ - user_id
+ - username
+ type: object
+ service_user.UserInfoUpdateData:
+ properties:
+ allow_public:
+ type: boolean
+ avatar:
+ type: string
+ bio:
+ type: string
+ nickname:
+ type: string
+ permission_level:
+ type: integer
+ subtitle:
+ type: string
+ user_id:
+ type: string
+ username:
+ type: string
+ type: object
+ service_user.UserListResponse:
+ properties:
+ avatar:
+ type: string
+ email:
+ type: string
+ nickname:
+ type: string
+ permission_level:
+ type: integer
+ subtitle:
+ type: string
+ user_id:
+ type: string
+ username:
+ type: string
+ type: object
+ utils.RespStatus:
+ properties:
+ code:
+ type: integer
+ data: {}
+ error_id:
+ type: string
+ status:
+ type: string
+ required:
+ - code
+ - data
+ - error_id
+ - status
+ type: object
+host: localhost:8000
+info:
+ 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:
+ /agenda/list:
+ get:
+ consumes:
+ - application/json
+ description: Returns all agendas for the specified event, regardless of status.
+ Manager only.
+ parameters:
+ - description: Event ID
+ in: query
+ name: event_id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ items:
+ $ref: '#/definitions/service_agenda.AgendaListItem'
+ type: array
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: List All Agendas
+ tags:
+ - Agenda
+ /agenda/my-list:
+ get:
+ consumes:
+ - application/json
+ description: Returns the calling user's agenda submissions for the specified
+ event. User must be a joined attendee (Lv10+).
+ parameters:
+ - description: Event ID
+ in: query
+ name: event_id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ items:
+ $ref: '#/definitions/data.Agenda'
+ type: array
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Not an Attendee
+ 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
+ security:
+ - Bearer: []
+ summary: My Agenda List
+ tags:
+ - Agenda
+ /agenda/review:
+ patch:
+ consumes:
+ - application/json
+ description: Manager sets the status of an agenda to approved or rejected. Not
+ allowed after agenda is published.
+ parameters:
+ - description: Review Data
+ in: body
+ name: body
+ required: true
+ schema:
+ $ref: '#/definitions/service_agenda.AgendaReviewData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input / Already Published
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event or Agenda Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Review Agenda
+ tags:
+ - Agenda
+ /agenda/schedule:
+ get:
+ description: Returns all approved and scheduled agenda items, sorted by start_time
+ ascending. Returns 403 if the agenda has not been published.
+ parameters:
+ - description: Event ID
+ in: query
+ name: event_id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ items:
+ $ref: '#/definitions/data.AgendaDoc'
+ type: array
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Agenda Not Published
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Get Agenda Schedule
+ tags:
+ - Agenda
+ patch:
+ consumes:
+ - application/json
+ description: Manager sets start_time and end_time on an approved agenda item.
+ Available even after publish.
+ parameters:
+ - description: Schedule Data
+ in: body
+ name: body
+ required: true
+ schema:
+ $ref: '#/definitions/service_agenda.AgendaScheduleData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input / Not Approved
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Agenda Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Schedule Agenda
+ tags:
+ - Agenda
+ /agenda/submit:
+ post:
+ consumes:
+ - application/json
+ description: Creates a new agenda item for a specific attendance record.
+ parameters:
+ - description: Agenda Submission Data
+ in: body
+ name: body
+ required: true
+ schema:
+ $ref: '#/definitions/service_agenda.SubmitData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_agenda.SubmitResponse'
+ 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
+ security:
+ - Bearer: []
+ summary: Submit Agenda
+ tags:
+ - Agenda
+ /agenda/update:
+ patch:
+ consumes:
+ - application/json
+ description: Submitter may edit their own pending agendas before the event deadline.
+ Managers may edit any agenda with no restrictions.
+ parameters:
+ - description: Agenda Update Data
+ in: body
+ name: body
+ required: true
+ schema:
+ $ref: '#/definitions/service_agenda.AgendaUpdateData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input / Not Pending / Deadline Passed
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Not Submitter
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Agenda Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Update Agenda
+ tags:
+ - Agenda
+ /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: Successful exchange
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_auth.ExchangeResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ 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
+ security:
+ - Bearer: []
+ 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: Successful request
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_auth.MagicResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Turnstile Verification Failed
+ 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: 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:
+ - application/json
+ - 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:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Invalid or Expired Verification Code
+ 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: 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: Successful rotation
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_auth.TokenResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Invalid Refresh Token
+ 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: 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: Successful token issuance
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_auth.TokenResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Invalid or Expired Code
+ 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: Exchange Code for Token
+ tags:
+ - Authentication
+ /event/attendance:
+ get:
+ description: Retrieves the paginated list of attendees with optional filters.
+ Only accessible by the event owner (Manager). Supports name substring search
+ and KYC status filtering.
+ parameters:
+ - description: Event UUID
+ in: query
+ name: event_id
+ required: true
+ type: string
+ - description: Substring filter on attendee nickname
+ in: query
+ name: name
+ type: string
+ - description: 'KYC filter: ''with_kyc'' or ''without_kyc'''
+ in: query
+ name: kyc_status
+ type: string
+ - description: Maximum number of results to return (default 20)
+ in: query
+ name: limit
+ type: integer
+ - description: Number of results to skip (default 0)
+ in: query
+ name: offset
+ type: integer
+ - description: 'Sort field: ''checkin_at'' (default) or ''id'''
+ in: query
+ name: sort_by
+ type: string
+ - description: 'Sort direction: ''asc'' or ''desc'' (default)'
+ in: query
+ name: sort_order
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successful retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ items:
+ $ref: '#/definitions/service_event.AttendanceListResponse'
+ type: array
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Not Event Owner
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Get Attendance List
+ tags:
+ - Event
+ /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: Successfully generated code
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.CheckinResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ 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
+ security:
+ - Bearer: []
+ 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: Current attendance status
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.CheckinQueryResponse'
+ 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
+ /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: Attendance marked successfully
+ schema:
+ 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
+ security:
+ - Bearer: []
+ summary: Submit Check-in Code
+ tags:
+ - Event
+ /event/create:
+ post:
+ consumes:
+ - application/json
+ description: Allows a Lv30+ user to create a new event. Users at exactly Lv30
+ may only create events with type 'party'. Sets type and enable_kyc, which
+ are immutable after creation.
+ parameters:
+ - description: Event Creation Details
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/service_event.EventCreateData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successfully created the event
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.EventCreateResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Permission Denied / Type Not Allowed for this level
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "500":
+ description: Internal Server Error / Database Error
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ security:
+ - Bearer: []
+ summary: Create an Event
+ tags:
+ - Event
+ /event/delete:
+ delete:
+ consumes:
+ - application/json
+ description: Permanently deletes an event. Requires Lv40+.
+ parameters:
+ - description: Event to delete
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/service_event.EventDeleteData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successfully deleted
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Delete an Event
+ tags:
+ - Event
+ /event/guide:
+ get:
+ consumes:
+ - application/json
+ description: Fetching attendance guide 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: Successful retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.AttendanceGuideResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Get Event Guide
+ 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: Successful retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.EventInfoResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Get Event Information
+ tags:
+ - Event
+ /event/join:
+ post:
+ consumes:
+ - application/json
+ description: Allows an authenticated user to join an event by providing the
+ event ID. The user's role and state are initialized by the service.
+ parameters:
+ - description: Event Join Details (UserId and EventId are required)
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/service_event.EventJoinData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successfully joined the event
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.EventJoinResponse'
+ type: object
+ "400":
+ description: Invalid Input or UUID Parse Failed
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Unauthorized / Missing User ID / Event Limit Exceeded
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "500":
+ description: Internal Server Error / Database Error
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ security:
+ - Bearer: []
+ summary: Join an Event
+ tags:
+ - Event
+ /event/list:
+ get:
+ consumes:
+ - application/json
+ description: Returns a paginated list of events. Supports filtering by type
+ and sorting. Lv30 users are automatically scoped to events they own.
+ parameters:
+ - description: Maximum number of events to return (default 20)
+ in: query
+ name: limit
+ type: integer
+ - description: Number of events to skip
+ in: query
+ name: offset
+ required: true
+ type: integer
+ - description: 'Filter by event type: ''official'' or ''party'''
+ in: query
+ name: type
+ type: string
+ - description: 'Sort field: ''start_time'' (default), ''end_time'', ''name'''
+ in: query
+ name: sort_by
+ type: string
+ - description: 'Sort direction: ''asc'' or ''desc'' (default)'
+ in: query
+ name: sort_order
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successful paginated list retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.EventListResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ 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
+ security:
+ - Bearer: []
+ summary: List Events
+ tags:
+ - Event
+ /event/stats:
+ get:
+ description: Returns join count, checkin count, KYC pass rate, and agenda submission
+ count. Only accessible by the event owner (Manager).
+ parameters:
+ - description: Event UUID
+ in: query
+ name: event_id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Statistics retrieved successfully
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_event.EventStatsResponse'
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Not Event Owner
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Get Event Statistics
+ tags:
+ - Event
+ /event/update:
+ patch:
+ consumes:
+ - application/json
+ description: 'Allows the event owner (Manager) to update name, subtitle, description,
+ start_time, end_time, thumbnail, and is_agenda_published. Changes to type
+ or enable_kyc are rejected. is_agenda_published is write-once: it can only
+ be set to true (requires at least one agenda submission) and cannot be reverted.'
+ parameters:
+ - description: Fields to update (all optional except event_id)
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/service_event.EventUpdateData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successfully updated
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input / Immutable Field / Agenda Pre-flight Failed
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Not Event Owner
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Event Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Update an Event
+ tags:
+ - Event
+ /kyc/query:
+ post:
+ consumes:
+ - application/json
+ description: Checks the current state of a KYC session and updates local database
+ if approved.
+ parameters:
+ - description: KYC query data (KycId)
+ in: body
+ name: payload
+ required: true
+ schema:
+ $ref: '#/definitions/service_kyc.KycQueryData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Query processed (success/pending/failed)
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_kyc.KycQueryResponse'
+ type: object
+ "400":
+ description: Invalid UUID or input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Unauthorized
+ 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
+ security:
+ - Bearer: []
+ summary: Query KYC Status
+ tags:
+ - KYC
+ /kyc/session:
+ post:
+ consumes:
+ - application/json
+ description: Initializes a KYC process (CNRid or Passport) and returns the status
+ or redirect URI.
+ parameters:
+ - description: KYC session data (Type and Base64 Identity)
+ in: body
+ name: payload
+ required: true
+ schema:
+ $ref: '#/definitions/service_kyc.KycSessionData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Session created successfully
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_kyc.KycSessionResponse'
+ type: object
+ "400":
+ description: Invalid input or decode failed
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Missing User ID
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "500":
+ description: Internal Server Error / KYC Service Error
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ security:
+ - Bearer: []
+ summary: Create KYC Session
+ tags:
+ - KYC
+ /stats/global:
+ get:
+ description: Returns total users, user counts per permission_level, and per-event
+ join/checkin counts.
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_stats.GlobalStatsResponse'
+ type: object
+ "401":
+ description: Unauthorized
+ 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
+ security:
+ - Bearer: []
+ summary: Global Stats
+ tags:
+ - Stats
+ /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: Successful profile retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_user.UserInfoData'
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: User Not Found
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "500":
+ description: Internal Server Error (UUID Parse Failed)
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ security:
+ - Bearer: []
+ summary: Get My User Information
+ tags:
+ - User
+ /user/info/{user_id}:
+ get:
+ consumes:
+ - application/json
+ description: Fetches the complete profile data for the user associated with
+ the provided session/token.
+ parameters:
+ - description: Other user id
+ in: path
+ name: user_id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successful profile retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ $ref: '#/definitions/service_user.UserInfoData'
+ type: object
+ "401":
+ description: Missing User ID / Unauthorized
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: User Not Public
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: User Not Found
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "500":
+ description: Internal Server Error (UUID Parse Failed)
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ security:
+ - Bearer: []
+ summary: Get Other User Information
+ tags:
+ - User
+ /user/list:
+ get:
+ consumes:
+ - application/json
+ description: Returns a paginated list of users with permission_level included.
+ Supports filtering by permission_level and sorting.
+ parameters:
+ - description: Maximum number of users to return (default 20)
+ in: query
+ name: limit
+ type: string
+ - description: Number of users to skip
+ in: query
+ name: offset
+ required: true
+ type: string
+ - description: 'Sort field: ''id'' (default) | ''permission_level'''
+ in: query
+ name: sort_by
+ type: string
+ - description: 'Sort direction: ''asc'' (default) | ''desc'''
+ in: query
+ name: sort_order
+ type: string
+ - description: Filter by exact permission level
+ in: query
+ name: permission_level
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successful paginated list retrieval
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ items:
+ $ref: '#/definitions/service_user.UserListResponse'
+ type: array
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ 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
+ security:
+ - Bearer: []
+ summary: List Users (Admin)
+ 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.UserInfoUpdateData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Successful profile update
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "401":
+ description: Unauthorized
+ 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
+ security:
+ - Bearer: []
+ summary: Update User Information
+ tags:
+ - User
+ /user/update/{user_id}:
+ patch:
+ consumes:
+ - application/json
+ description: 'Lv40+ operators may update any user with a strictly lower permission_level.
+ Editable fields: all profile fields plus permission_level (new value must
+ be below operator''s own level).'
+ parameters:
+ - description: Target User ID
+ in: path
+ name: user_id
+ required: true
+ type: string
+ - description: Fields to update
+ in: body
+ name: payload
+ required: true
+ schema:
+ $ref: '#/definitions/service_user.UserInfoUpdateData'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "400":
+ description: Invalid Input
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "403":
+ description: Permission Matrix Violation
+ schema:
+ allOf:
+ - $ref: '#/definitions/utils.RespStatus'
+ - properties:
+ data:
+ type: object
+ type: object
+ "404":
+ description: Target User Not Found
+ 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
+ security:
+ - Bearer: []
+ summary: Admin Update User
+ tags:
+ - User
+schemes:
+- http
+- https
+securityDefinitions:
+ Bearer:
+ in: header
+ name: Authorization
+ type: apiKey
+swagger: "2.0"
diff --git a/generate.go b/generate.go
index 64e68fb..561b1bd 100644
--- a/generate.go
+++ b/generate.go
@@ -1,3 +1,6 @@
package main
//go:generate go run ./cmd/gen_exception/main.go
+
+//go:generate swag fmt
+//go:generate swag init -g server/server.go
diff --git a/go.mod b/go.mod
index c345d84..6bed472 100644
--- a/go.mod
+++ b/go.mod
@@ -2,24 +2,33 @@ module nixcn-cms
go 1.25.5
+replace (
+ google.golang.org/genproto => google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1
+ google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1
+ google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1
+)
+
require (
- github.com/alibabacloud-go/cloudauth-20190307/v4 v4.12.1
- github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
- github.com/alibabacloud-go/tea v1.3.13
- github.com/alibabacloud-go/tea-utils/v2 v2.0.7
- github.com/aliyun/credentials-go v1.4.5
+ github.com/alibabacloud-go/cloudauth-20190307/v4 v4.13.1
+ github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14
+ github.com/alibabacloud-go/tea v1.4.0
+ github.com/alibabacloud-go/tea-utils/v2 v2.0.9
+ github.com/alicebob/miniredis/v2 v2.37.0
+ github.com/aliyun/credentials-go v1.4.11
github.com/gin-gonic/gin v1.11.0
- github.com/go-viper/mapstructure/v2 v2.4.0
+ github.com/glebarez/sqlite v1.11.0
github.com/goccy/go-json v0.10.5
- github.com/golang-jwt/jwt/v5 v5.3.0
+ github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
- github.com/meilisearch/meilisearch-go v0.35.0
- github.com/redis/go-redis/extra/redisotel/v9 v9.17.2
- github.com/redis/go-redis/v9 v9.17.2
+ github.com/redis/go-redis/extra/redisotel/v9 v9.17.3
+ github.com/redis/go-redis/v9 v9.17.3
github.com/spf13/viper v1.21.0
+ github.com/stretchr/testify v1.11.1
+ 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
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
@@ -29,7 +38,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,9 +52,11 @@ 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.1.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
@@ -54,21 +65,27 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
- github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
- github.com/gabriel-vasile/mimetype v1.4.12 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
+ github.com/go-openapi/spec v0.20.4 // indirect
+ github.com/go-openapi/swag v0.19.15 // 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.28.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
- github.com/goccy/go-yaml v1.19.1 // indirect
- github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
+ github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+ github.com/goccy/go-yaml v1.19.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -77,10 +94,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.7.6 // 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
@@ -88,9 +107,11 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
- github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 // indirect
+ github.com/redis/go-redis/extra/rediscmd/v9 v9.17.3 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
@@ -102,21 +123,29 @@ require (
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
+ github.com/yuin/gopher-lua v1.1.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
+ golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
- golang.org/x/sys v0.39.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
+ golang.org/x/sys v0.40.0 // indirect
+ golang.org/x/tools v0.40.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
- google.golang.org/protobuf v1.36.11 // indirect
+ google.golang.org/protobuf v1.36.10 // 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
+ modernc.org/libc v1.22.5 // indirect
+ modernc.org/mathutil v1.5.0 // indirect
+ modernc.org/memory v1.5.0 // indirect
+ modernc.org/sqlite v1.23.1 // indirect
)
diff --git a/go.sum b/go.sum
index 9901b74..01896f9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,26 +1,327 @@
+cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
+cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
+cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
+cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
+cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
+cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
+cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
+cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
+cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
+cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
+cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk=
+cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
+cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
+cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
+cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E=
+cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
+cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
+cloud.google.com/go/accessapproval v1.8.0/go.mod h1:ycc7qSIXOrH6gGOGQsuBwpRZw3QhZLi0OWeej3rA5Mg=
+cloud.google.com/go/accesscontextmanager v1.8.12/go.mod h1:EmaVYmffq+2jA2waP0/XHECDkaOKVztxVsdzl65t8hw=
+cloud.google.com/go/accesscontextmanager v1.9.0/go.mod h1:EmdQRGq5FHLrjGjGTp2X2tlRBvU3LDCUqfnysFYooxQ=
+cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME=
+cloud.google.com/go/analytics v0.25.0/go.mod h1:LZMfjJnKU1GDkvJV16dKnXm7KJJaMZfvUXx58ujgVLg=
+cloud.google.com/go/apigateway v1.7.0/go.mod h1:miZGNhmrC+SFhxjA7ayjKHk1cA+7vsSINp9K+JxKwZI=
+cloud.google.com/go/apigeeconnect v1.7.0/go.mod h1:fd8NFqzu5aXGEUpxiyeCyb4LBLU7B/xIPztfBQi+1zg=
+cloud.google.com/go/apigeeregistry v0.9.0/go.mod h1:4S/btGnijdt9LSIZwBDHgtYfYkFGekzNyWkyYTP8Qzs=
+cloud.google.com/go/appengine v1.9.0/go.mod h1:y5oI+JT3/6s77QmxbTnLHyiMKz3NPHYOjuhmVi+FyYU=
+cloud.google.com/go/area120 v0.9.0/go.mod h1:ujIhRz2gJXutmFYGAUgz3KZ5IRJ6vOwL4CYlNy/jDo4=
+cloud.google.com/go/artifactregistry v1.15.0/go.mod h1:4xrfigx32/3N7Pp7YSPOZZGs4VPhyYeRyJ67ZfVdOX4=
+cloud.google.com/go/asset v1.20.0/go.mod h1:CT3ME6xNZKsPSvi0lMBPgW3azvRhiurJTFSnNl6ahw8=
+cloud.google.com/go/assuredworkloads v1.12.0/go.mod h1:jX84R+0iANggmSbzvVgrGWaqdhRsQihAv4fF7IQ4r7Q=
+cloud.google.com/go/auth v0.2.1/go.mod h1:khQRBNrvNoHiHhV1iu2x8fSnlNbCaVHilznW5MAI5GY=
+cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
+cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
+cloud.google.com/go/auth v0.4.2/go.mod h1:Kqvlz1cf1sNA0D+sYJnkPQOP+JMHkuHeIgVmCRtZOLc=
+cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
+cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g=
+cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4=
+cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
+cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
+cloud.google.com/go/auth v0.7.3/go.mod h1:HJtWUx1P5eqjy/f6Iq5KeytNpbAcGolPhOgyop2LlzA=
+cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
+cloud.google.com/go/auth v0.9.0/go.mod h1:2HsApZBr9zGZhC9QAXsYVYaWk8kNUt37uny+XVKi7wM=
+cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk=
+cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
+cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
+cloud.google.com/go/automl v1.14.0/go.mod h1:Kr7rN9ANSjlHyBLGvwhrnt35/vVZy3n/CP4Xmyj0shM=
+cloud.google.com/go/baremetalsolution v1.3.0/go.mod h1:E+n44UaDVO5EeSa4SUsDFxQLt6dD1CoE2h+mtxxaJKo=
+cloud.google.com/go/batch v1.10.0/go.mod h1:JlktZqyKbcUJWdHOV8juvAiQNH8xXHXTqLp6bD9qreE=
+cloud.google.com/go/beyondcorp v1.1.0/go.mod h1:F6Rl20QbayaloWIsMhuz+DICcJxckdFKc7R2HCe6iNA=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/bigquery v1.62.0/go.mod h1:5ee+ZkF1x/ntgCsFQJAQTM3QkAZOecfCmvxhkJsWRSA=
+cloud.google.com/go/bigtable v1.18.1/go.mod h1:NAVyfJot9jlo+KmgWLUJ5DJGwNDoChzAcrecLpmuAmY=
+cloud.google.com/go/bigtable v1.20.0/go.mod h1:upJDn8frsjzpRMfybiWkD1PG6WCCL7CRl26MgVeoXY4=
+cloud.google.com/go/bigtable v1.31.0/go.mod h1:N/mwZO+4TSHOeyiE1JxO+sRPnW4bnR7WLn9AEaiJqew=
+cloud.google.com/go/billing v1.19.0/go.mod h1:bGvChbZguyaWRGmu5pQHfFN1VxTDPFmabnCVA/dNdRM=
+cloud.google.com/go/binaryauthorization v1.9.0/go.mod h1:fssQuxfI9D6dPPqfvDmObof+ZBKsxA9iSigd8aSA1ik=
+cloud.google.com/go/certificatemanager v1.9.0/go.mod h1:hQBpwtKNjUq+er6Rdg675N7lSsNGqMgt7Bt7Dbcm7d0=
+cloud.google.com/go/channel v1.18.0/go.mod h1:gQr50HxC/FGvufmqXD631ldL1Ee7CNMU5F4pDyJWlt0=
+cloud.google.com/go/cloudbuild v1.17.0/go.mod h1:/RbwgDlbQEwIKoWLIYnW72W3cWs+e83z7nU45xRKnj8=
+cloud.google.com/go/clouddms v1.8.0/go.mod h1:JUgTgqd1M9iPa7p3jodjLTuecdkGTcikrg7nz++XB5E=
+cloud.google.com/go/cloudtasks v1.13.0/go.mod h1:O1jFRGb1Vm3sN2u/tBdPiVGVTWIsrsbEs3K3N3nNlEU=
+cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
+cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
+cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
+cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
+cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
+cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
+cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
+cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
+cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
+cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
+cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
+cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
+cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
+cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI=
+cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
+cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
+cloud.google.com/go/compute v1.28.0/go.mod h1:DEqZBtYrDnD5PvjsKwb3onnhX+qjdCVM7eshj1XdjV4=
+cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
+cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
+cloud.google.com/go/contactcenterinsights v1.14.0/go.mod h1:APmWYHDN4sASnUBnXs4o68t1EUfnqadA53//CzXZ1xE=
+cloud.google.com/go/container v1.39.0/go.mod h1:gNgnvs1cRHXjYxrotVm+0nxDfZkqzBbXCffh5WtqieI=
+cloud.google.com/go/containeranalysis v0.13.0/go.mod h1:OpufGxsNzMOZb6w5yqwUgHr5GHivsAD18KEI06yGkQs=
+cloud.google.com/go/datacatalog v1.20.3/go.mod h1:AKC6vAy5urnMg5eJK3oUjy8oa5zMbiY33h125l8lmlo=
+cloud.google.com/go/datacatalog v1.22.0/go.mod h1:4Wff6GphTY6guF5WphrD76jOdfBiflDiRGFAxq7t//I=
+cloud.google.com/go/dataflow v0.10.0/go.mod h1:zAv3YUNe/2pXWKDSPvbf31mCIUuJa+IHtKmhfzaeGww=
+cloud.google.com/go/dataform v0.10.0/go.mod h1:0NKefI6v1ppBEDnwrp6gOMEA3s/RH3ypLUM0+YWqh6A=
+cloud.google.com/go/datafusion v1.8.0/go.mod h1:zHZ5dJYHhMP1P8SZDZm+6yRY9BCCcfm7Xg7YmP+iA6E=
+cloud.google.com/go/datalabeling v0.9.0/go.mod h1:GVX4sW4cY5OPKu/9v6dv20AU9xmGr4DXR6K26qN0mzw=
+cloud.google.com/go/dataplex v1.19.0/go.mod h1:5H9ftGuZWMtoEIUpTdGUtGgje36YGmtRXoC8wx6QSUc=
+cloud.google.com/go/dataproc/v2 v2.6.0/go.mod h1:amsKInI+TU4GcXnz+gmmApYbiYM4Fw051SIMDoWCWeE=
+cloud.google.com/go/dataqna v0.9.0/go.mod h1:WlRhvLLZv7TfpONlb/rEQx5Qrr7b5sxgSuz5NP6amrw=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/datastore v1.19.0/go.mod h1:KGzkszuj87VT8tJe67GuB+qLolfsOt6bZq/KFuWaahc=
+cloud.google.com/go/datastream v1.11.0/go.mod h1:vio/5TQ0qNtGcIj7sFb0gucFoqZW19gZ7HztYtkzq9g=
+cloud.google.com/go/deploy v1.22.0/go.mod h1:qXJgBcnyetoOe+w/79sCC99c5PpHJsgUXCNhwMjG0e4=
+cloud.google.com/go/dialogflow v1.57.0/go.mod h1:wegtnocuYEfue6IGlX96n5mHu3JGZUaZxv1L5HzJUJY=
+cloud.google.com/go/dlp v1.18.0/go.mod h1:RVO9zkh+xXgUa7+YOf9IFNHL/2FXt9Vnv/GKNYmc1fE=
+cloud.google.com/go/documentai v1.33.0/go.mod h1:lI9Mti9COZ5qVjdpfDZxNjOrTVf6tJ//vaqbtt81214=
+cloud.google.com/go/domains v0.10.0/go.mod h1:VpPXnkCNRsxkieDFDfjBIrLv3p1kRjJ03wLoPeL30To=
+cloud.google.com/go/edgecontainer v1.3.0/go.mod h1:dV1qTl2KAnQOYG+7plYr53KSq/37aga5/xPgOlYXh3A=
+cloud.google.com/go/errorreporting v0.3.1/go.mod h1:6xVQXU1UuntfAf+bVkFk6nld41+CPyF2NSPCyXE3Ztk=
+cloud.google.com/go/essentialcontacts v1.7.0/go.mod h1:0JEcNuyjyg43H/RJynZzv2eo6MkmnvRPUouBpOh6akY=
+cloud.google.com/go/eventarc v1.14.0/go.mod h1:60ZzZfOekvsc/keHc7uGHcoEOMVa+p+ZgRmTjpdamnA=
+cloud.google.com/go/filestore v1.9.0/go.mod h1:GlQK+VBaAGb19HqprnOMqYYpn7Gev5ZA9SSHpxFKD7Q=
+cloud.google.com/go/firestore v1.16.0/go.mod h1:+22v/7p+WNBSQwdSwP57vz47aZiY+HrDkrOsJNhk7rg=
+cloud.google.com/go/functions v1.19.0/go.mod h1:WDreEDZoUVoOkXKDejFWGnprrGYn2cY2KHx73UQERC0=
+cloud.google.com/go/gkebackup v1.6.0/go.mod h1:1rskt7NgawoMDHTdLASX8caXXYG3MvDsoZ7qF4RMamQ=
+cloud.google.com/go/gkeconnect v0.11.0/go.mod h1:l3iPZl1OfT+DUQ+QkmH1PC5RTLqxKQSVnboLiQGAcCA=
+cloud.google.com/go/gkehub v0.15.0/go.mod h1:obpeROly2mjxZJbRkFfHEflcH54XhJI+g2QgfHphL0I=
+cloud.google.com/go/gkemulticloud v1.3.0/go.mod h1:XmcOUQ+hJI62fi/klCjEGs6lhQ56Zjs14sGPXsGP0mE=
+cloud.google.com/go/grafeas v0.3.10/go.mod h1:Mz/AoXmxNhj74VW0fz5Idc3kMN2VZMi4UT5+UPx5Pq0=
+cloud.google.com/go/gsuiteaddons v1.7.0/go.mod h1:/B1L8ANPbiSvxCgdSwqH9CqHIJBzTt6v50fPr3vJCtg=
+cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
+cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
+cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
+cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
+cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY=
+cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
+cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
+cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE=
+cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
+cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
+cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
+cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
+cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
+cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=
+cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
+cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q=
+cloud.google.com/go/iap v1.10.0/go.mod h1:gDT6LZnKnWNCaov/iQbj7NMUpknFDOkhhlH8PwIrpzU=
+cloud.google.com/go/ids v1.5.0/go.mod h1:4NOlC1m9hAJL50j2cRV4PS/J6x/f4BBM0Xg54JQLCWw=
+cloud.google.com/go/iot v1.8.0/go.mod h1:/NMFENPnQ2t1UByUC1qFvA80fo1KFB920BlyUPn1m3s=
+cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs=
+cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
+cloud.google.com/go/kms v1.19.0/go.mod h1:e4imokuPJUc17Trz2s6lEXFDt8bgDmvpVynH39bdrHM=
+cloud.google.com/go/language v1.14.0/go.mod h1:ldEdlZOFwZREnn/1yWtXdNzfD7hHi9rf87YDkOY9at4=
+cloud.google.com/go/lifesciences v0.10.0/go.mod h1:1zMhgXQ7LbMbA5n4AYguFgbulbounfUoYvkV8dtsLcA=
+cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A=
+cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE=
+cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
+cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
+cloud.google.com/go/longrunning v0.5.0/go.mod h1:0JNuqRShmscVAhIACGtskSAWtqtOoPkwP0YF1oVEchc=
+cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
+cloud.google.com/go/longrunning v0.5.2/go.mod h1:nqo6DQbNV2pXhGDbDMoN2bWz68MjZUzqv2YttZiveCs=
+cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
+cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
+cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
+cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
+cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
+cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=
+cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU=
+cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts=
+cloud.google.com/go/managedidentities v1.7.0/go.mod h1:o4LqQkQvJ9Pt7Q8CyZV39HrzCfzyX8zBzm8KIhRw91E=
+cloud.google.com/go/maps v1.12.0/go.mod h1:qjErDNStn3BaGx06vHner5d75MRMgGflbgCuWTuslMc=
+cloud.google.com/go/mediatranslation v0.9.0/go.mod h1:udnxo0i4YJ5mZfkwvvQQrQ6ra47vcX8jeGV+6I5x+iU=
+cloud.google.com/go/memcache v1.11.0/go.mod h1:99MVF02m5TByT1NKxsoKDnw5kYmMrjbGSeikdyfCYZk=
+cloud.google.com/go/metastore v1.14.0/go.mod h1:vtPt5oVF/+ocXO4rv4GUzC8Si5s8gfmo5OIt6bACDuE=
+cloud.google.com/go/monitoring v1.21.0/go.mod h1:tuJ+KNDdJbetSsbSGTqnaBvbauS5kr3Q/koy3Up6r+4=
+cloud.google.com/go/networkconnectivity v1.15.0/go.mod h1:uBQqx/YHI6gzqfV5J/7fkKwTGlXvQhHevUuzMpos9WY=
+cloud.google.com/go/networkmanagement v1.14.0/go.mod h1:4myfd4A0uULCOCGHL1npZN0U+kr1Z2ENlbHdCCX4cE8=
+cloud.google.com/go/networksecurity v0.10.0/go.mod h1:IcpI5pyzlZyYG8cNRCJmY1AYKajsd9Uz575HoeyYoII=
+cloud.google.com/go/notebooks v1.12.0/go.mod h1:euIZBbGY6G0J+UHzQ0XflysP0YoAUnDPZU7Fq0KXNw8=
+cloud.google.com/go/optimization v1.7.0/go.mod h1:6KvAB1HtlsMMblT/lsQRIlLjUhKjmMWNqV1AJUctbWs=
+cloud.google.com/go/orchestration v1.10.0/go.mod h1:pGiFgTTU6c/nXHTPpfsGT8N4Dax8awccCe6kjhVdWjI=
+cloud.google.com/go/orgpolicy v1.12.8/go.mod h1:WHkLGqHILPnMgJ4UTdag6YgztVIgWS+T5T6tywH3cSM=
+cloud.google.com/go/orgpolicy v1.13.0/go.mod h1:oKtT56zEFSsYORUunkN2mWVQBc9WGP7yBAPOZW1XCXc=
+cloud.google.com/go/osconfig v1.13.3/go.mod h1:gIFyyriC1ANob8SnpwrQ6jjNroRwItoBOYfqiG3LkUU=
+cloud.google.com/go/osconfig v1.14.0/go.mod h1:GhZzWYVrnQ42r+K5pA/hJCsnWVW2lB6bmVg+GnZ6JkM=
+cloud.google.com/go/oslogin v1.14.0/go.mod h1:VtMzdQPRP3T+w5OSFiYhaT/xOm7H1wo1HZUD2NAoVK4=
+cloud.google.com/go/phishingprotection v0.9.0/go.mod h1:CzttceTk9UskH9a8BycYmHL64zakEt3EXaM53r4i0Iw=
+cloud.google.com/go/policytroubleshooter v1.11.0/go.mod h1:yTqY8n60lPLdU5bRbImn9IazrmF1o5b0VBshVxPzblQ=
+cloud.google.com/go/privatecatalog v0.10.0/go.mod h1:/Lci3oPTxJpixjiTBoiVv3PmUZg/IdhPvKHcLEgObuc=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/pubsub v1.38.0/go.mod h1:IPMJSWSus/cu57UyR01Jqa/bNOQA+XnPF6Z4dKW4fAA=
+cloud.google.com/go/pubsub v1.42.0/go.mod h1:KADJ6s4MbTwhXmse/50SebEhE4SmUwHi48z3/dHar1Y=
+cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI=
+cloud.google.com/go/recaptchaenterprise/v2 v2.17.0/go.mod h1:SS4QDdlmJ3NvbOMCXQxaFhVGRjvNMfoKCoCdxqXadqs=
+cloud.google.com/go/recommendationengine v0.9.0/go.mod h1:59ydKXFyXO4Y8S0Bk224sKfj6YvIyzgcpG6w8kXIMm4=
+cloud.google.com/go/recommender v1.13.0/go.mod h1:+XkXkeB9k6zG222ZH70U6DBkmvEL0na+pSjZRmlWcrk=
+cloud.google.com/go/redis v1.17.0/go.mod h1:pzTdaIhriMLiXu8nn2CgiS52SYko0tO1Du4d3MPOG5I=
+cloud.google.com/go/resourcemanager v1.10.0/go.mod h1:kIx3TWDCjLnUQUdjQ/e8EXsS9GJEzvcY+YMOHpADxrk=
+cloud.google.com/go/resourcesettings v1.8.0/go.mod h1:/hleuSOq8E6mF1sRYZrSzib8BxFHprQXrPluWTuZ6Ys=
+cloud.google.com/go/retail v1.18.0/go.mod h1:vaCabihbSrq88mKGKcKc4/FDHvVcPP0sQDAt0INM+v8=
+cloud.google.com/go/run v1.5.0/go.mod h1:Z4Tv/XNC/veO6rEpF0waVhR7vEu5RN1uJQ8dD1PeMtI=
+cloud.google.com/go/scheduler v1.11.0/go.mod h1:RBSu5/rIsF5mDbQUiruvIE6FnfKpLd3HlTDu8aWk0jw=
+cloud.google.com/go/secretmanager v1.14.0/go.mod h1:q0hSFHzoW7eRgyYFH8trqEFavgrMeiJI4FETNN78vhM=
+cloud.google.com/go/security v1.18.0/go.mod h1:oS/kRVUNmkwEqzCgSmK2EaGd8SbDUvliEiADjSb/8Mo=
+cloud.google.com/go/securitycenter v1.35.0/go.mod h1:gotw8mBfCxX0CGrRK917CP/l+Z+QoDchJ9HDpSR8eDc=
+cloud.google.com/go/servicedirectory v1.12.0/go.mod h1:lKKBoVStJa+8S+iH7h/YRBMUkkqFjfPirkOTEyYAIUk=
+cloud.google.com/go/shell v1.8.0/go.mod h1:EoQR8uXuEWHUAMoB4+ijXqRVYatDCdKYOLAaay1R/yw=
+cloud.google.com/go/spanner v1.67.0/go.mod h1:Um+TNmxfcCHqNCKid4rmAMvoe/Iu1vdz6UfxJ9GPxRQ=
+cloud.google.com/go/speech v1.25.0/go.mod h1:2IUTYClcJhqPgee5Ko+qJqq29/bglVizgIap0c5MvYs=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
+cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
+cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
+cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
+cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
+cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
+cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY=
+cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
+cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
+cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
+cloud.google.com/go/storage v1.42.0/go.mod h1:HjMXRFq65pGKFn6hxj6x3HCyR41uSB72Z0SO/Vn6JFQ=
+cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
+cloud.google.com/go/storagetransfer v1.11.0/go.mod h1:arcvgzVC4HPcSikqV8D4h4PwrvGQHfKtbL4OwKPirjs=
+cloud.google.com/go/talent v1.7.0/go.mod h1:8zfRPWWV4GNZuUmBwQub0gWAe2KaKhsthyGtV8fV1bY=
+cloud.google.com/go/texttospeech v1.8.0/go.mod h1:hAgeA01K5QNfLy2sPUAVETE0L4WdEpaCMfwKH1qjCQU=
+cloud.google.com/go/tpu v1.7.0/go.mod h1:/J6Co458YHMD60nM3cCjA0msvFU/miCGMfx/nYyxv/o=
+cloud.google.com/go/trace v1.11.0/go.mod h1:Aiemdi52635dBR7o3zuc9lLjXo3BwGaChEjCa3tJNmM=
+cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs=
+cloud.google.com/go/translate v1.12.0/go.mod h1:4/C4shFIY5hSZ3b3g+xXWM5xhBLqcUqksSMrQ7tyFtc=
+cloud.google.com/go/video v1.23.0/go.mod h1:EGLQv3Ce/VNqcl/+Amq7jlrnpg+KMgQcr6YOOBfE9oc=
+cloud.google.com/go/videointelligence v1.12.0/go.mod h1:3rjmafNpCEqAb1CElGTA7dsg8dFDsx7RQNHS7o088D0=
+cloud.google.com/go/vision/v2 v2.9.0/go.mod h1:sejxShqNOEucObbGNV5Gk85hPCgiVPP4sWv0GrgKuNw=
+cloud.google.com/go/vmmigration v1.8.0/go.mod h1:+AQnGUabjpYKnkfdXJZ5nteUfzNDCmwbj/HSLGPFG5E=
+cloud.google.com/go/vmwareengine v1.3.0/go.mod h1:7W/C/YFpelGyZzRUfOYkbgUfbN1CK5ME3++doIkh1Vk=
+cloud.google.com/go/vpcaccess v1.8.0/go.mod h1:7fz79sxE9DbGm9dbbIdir3tsJhwCxiNAs8aFG8MEhR8=
+cloud.google.com/go/webrisk v1.10.0/go.mod h1:ztRr0MCLtksoeSOQCEERZXdzwJGoH+RGYQ2qodGOy2U=
+cloud.google.com/go/websecurityscanner v1.7.0/go.mod h1:d5OGdHnbky9MAZ8SGzdWIm3/c9p0r7t+5BerY5JYdZc=
+cloud.google.com/go/workflows v1.13.0/go.mod h1:StCuY3jhBj1HYMjCPqZs7J0deQLHPhF6hDtzWJaVF+Y=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo=
+github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
+github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+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/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/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
+github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
+github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
+github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
+github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
+github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
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=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
-github.com/alibabacloud-go/cloudauth-20190307/v4 v4.12.1 h1:bVUYai/CnqhFMcofU8TsJGnZEf9zCB1WakLxh0zmpjQ=
-github.com/alibabacloud-go/cloudauth-20190307/v4 v4.12.1/go.mod h1:m3NrP7nlncob5/2ATBk0thsZAozpDy/+vUG4ZuXwdR8=
+github.com/alibabacloud-go/cloudauth-20190307/v4 v4.13.1 h1:DGs+fId3Wwutu/a7QVGQtYT5KEgyH3B9qR8R5kE37rU=
+github.com/alibabacloud-go/cloudauth-20190307/v4 v4.13.1/go.mod h1:X0tj/tEYnmL+uSchHif9cV63X2INiIRy/4INO/MSOl4=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
-github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
-github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14 h1:iIamPRvehxQvVnTOvz77rZR+/YME1lR7X8kHonQSU6Y=
+github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
@@ -31,9 +332,8 @@ github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
+github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
-github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=
-github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
@@ -41,20 +341,31 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
-github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
+github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA=
+github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
+github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
-github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
-github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
+github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
+github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
+github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
-github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
+github.com/aliyun/credentials-go v1.4.11 h1:NajDnXYOFiYsAleYQoLl5Q+s5Yntp8PvOInNPlDzAtk=
+github.com/aliyun/credentials-go v1.4.11/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
+github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
+github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q=
+github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -68,102 +379,327 @@ github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCc
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230428030218-4003588d1b74/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM=
+github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
+github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
+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=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
+github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI=
+github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q=
+github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
+github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
+github.com/envoyproxy/go-control-plane v0.12.1-0.20240621013728-1eb8caab5155/go.mod h1:5Wkq+JduFtdAXihLmeTJf+tRYIT4KBc2vPXDhwVo1pA=
+github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
+github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
+github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
+github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
+github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
+github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
+github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
+github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
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/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
+github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+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=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
+github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
+github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
+github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
+github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
+github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
+github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
+github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
+github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/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/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+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-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
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.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
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.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
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.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
+github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
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=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=
+github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
+github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
+github.com/goccy/go-yaml v1.19.0/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=
-github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
-github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
+github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
+github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
+github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
+github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
+github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/cloud-bigtable-clients-test v0.0.0-20221104150409-300c96f7b1f5/go.mod h1:Udm7et5Lt9Xtzd4n07/kKP80IdlR4zVDjtlUZEO2Dd8=
+github.com/googleapis/cloud-bigtable-clients-test v0.0.0-20230505150253-16eeee810d3a/go.mod h1:2n/InOx7Q1jaqXZJ0poJmsZxb6K+OfHEbhA/+LPJrII=
+github.com/googleapis/cloud-bigtable-clients-test v0.0.2/go.mod h1:mk3CrkrouRgtnhID6UZQDK3DrFFa7cYCAJcEmNsHYrY=
+github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
+github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
+github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
+github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
+github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
+github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
+github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
+github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
+github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
+github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
+github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
+github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw=
+github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
+github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
+github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
+github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
+github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
+github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -176,34 +712,68 @@ 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=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
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/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
+github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
+github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
+github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
+github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
+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/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/meilisearch/meilisearch-go v0.35.0 h1:Gh4vO+PinVQZ58iiFdUX9Hld8uXKzKh+C7mSSsCDlI8=
-github.com/meilisearch/meilisearch-go v0.35.0/go.mod h1:cUVJZ2zMqTvvwIMEEAdsWH+zrHsrLpAw6gm8Lt1MXK0=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -218,25 +788,47 @@ github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/En
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
+github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
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/redis/go-redis/extra/rediscmd/v9 v9.17.2 h1:KYWnHK9pwzOUo3sNJlNmzRwZ5mw7opugn8njtGThKNg=
-github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2/go.mod h1:wsfMQVl/GFYD9Gx/tlxurlTtvHkZRAt8j1qi27eIlTk=
-github.com/redis/go-redis/extra/redisotel/v9 v9.17.2 h1:wthFPRW3Y50CknMrjjJoYwXUFR4U7hMVJCMeLzDI8s4=
-github.com/redis/go-redis/extra/redisotel/v9 v9.17.2/go.mod h1:iqfQX7U2o8MWSl8W+Ah8KqbQyi/UoR/MQNgvaUyA1wc=
-github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
-github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
+github.com/redis/go-redis/extra/rediscmd/v9 v9.17.3 h1:v9RNP5ynWkruvzscrIoDyyv20c9YeyVn12L9nYnaexw=
+github.com/redis/go-redis/extra/rediscmd/v9 v9.17.3/go.mod h1:gdthSemCkR3WxTmzV2XxYIxClunkUJZAhL0zPHaB0Ww=
+github.com/redis/go-redis/extra/redisotel/v9 v9.17.3 h1:bF0e3fV7PL0knd1UHDtMud8wA7CZt3RSWtyTMhpnWd8=
+github.com/redis/go-redis/extra/redisotel/v9 v9.17.3/go.mod h1:gR39sPK/dJZlqgIA9Nm4JFHcQJPyhsISBLj708nrD4w=
+github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
+github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@@ -248,6 +840,11 @@ github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
+github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -256,24 +853,42 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg=
+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.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
+github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@@ -287,21 +902,55 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
+github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
+go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0 h1:7IKZbAYwlwLXAdu7SVPhzTjDjogWZxP4MIa7rovY+PU=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0/go.mod h1:+TF5nf3NIv2X8PGxqfYOaRnAoMM43rUA2C3XsN2DoWA=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
+go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM=
@@ -316,18 +965,42 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
+go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
+go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
+go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
+go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
+go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
+go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -339,167 +1012,714 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
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.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+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-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
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-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
+golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
+golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
+golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
+golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.19.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.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
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=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
+golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
+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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
+gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
+gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
+google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
+google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
+google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
+google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
+google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
+google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
+google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
+google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08=
+google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
+google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
+google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
+google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
+google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
+google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
+google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
+google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
+google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E=
+google.golang.org/api v0.121.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
+google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
+google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4=
+google.golang.org/api v0.125.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
+google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
+google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750=
+google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU=
+google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI=
+google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg=
+google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk=
+google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw=
+google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
+google.golang.org/api v0.164.0/go.mod h1:2OatzO7ZDQsoS7IFf3rvsE17/TldiU3F/zxFHeqUB5o=
+google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
+google.golang.org/api v0.167.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
+google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
+google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
+google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg=
+google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw=
+google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U=
+google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
+google.golang.org/api v0.182.0/go.mod h1:cGhjy4caqA5yXRzEhkHI8Y9mfyC2VLTlER2l08xaqtM=
+google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
+google.golang.org/api v0.184.0/go.mod h1:CeDTtUEiYENAf8PPG5VZW2yNp2VM3VWbCeTioAZBTBA=
+google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc=
+google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
+google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
+google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
+google.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E=
+google.golang.org/api v0.193.0/go.mod h1:Po3YMV1XZx+mTku3cfJrlIYR03wiGrCOsdpC67hjZvw=
+google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
-google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:+34luvCflYKiKylNwGJfn9cFBbcL/WrkciMmDmsTQ/A=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0/go.mod h1:guYXGPwC6jwxgWKW5Y405fKWOFNwlvUlUnzyp9i0uqo=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:ZSvZ8l+AWJwXw91DoTjWjaVLpWU6o0eZ4YLYpH8aLeQ=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:SCz6T5xjNXM4QFPRwxHcfChp7V+9DcXR3ay2TkHR8Tg=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240205150955-31a09d347014/go.mod h1:EhZbXt+eY4Yr3YVaEGLdNZF5viWowOJZ8KTPqjYMKzg=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:om8Bj876Z0v9ei+RD1LnEWig7vpHQ371PUqsgjmLQEA=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240311132316-a219d84964c2/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:IN9OQUXZ0xT+26MDwZL8fJcYw+y99b0eYPA2U15Jt8o=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240429193739-8cf5692501f6/go.mod h1:ULqtoQMxDLNRfW+pJbKA68wtIy1OiYjdIsJs3PMpzh8=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240521202816-d264139d666e/go.mod h1:0J6mmn3XAEjfNbPvpH63c0RXCjGNFcCzlEfWSN4In+k=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240528184218-531527333157/go.mod h1:0J6mmn3XAEjfNbPvpH63c0RXCjGNFcCzlEfWSN4In+k=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240604185151-ef581f913117/go.mod h1:0J6mmn3XAEjfNbPvpH63c0RXCjGNFcCzlEfWSN4In+k=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240617180043-68d350f18fd4/go.mod h1:/oe3+SiHAwz6s+M25PyTygWm3lnrhmGqIuIfkoUocqk=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:/oe3+SiHAwz6s+M25PyTygWm3lnrhmGqIuIfkoUocqk=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240708141625-4ad9e859172b/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240722135656-d784300faade/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ=
+google.golang.org/genproto/googleapis/bytestream v0.0.0-20240814211410-ddb44dafa142/go.mod h1:gQizMG9jZ0L2ADJaM+JdZV4yTCON/CQpnHRPoM+54w4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
+google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
+google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
+google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
+google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
+google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
+google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
+google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
+google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
+google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
+google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
+google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
-google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -507,14 +1727,19 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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.3/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=
@@ -534,5 +1759,53 @@ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc=
gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
+lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
+modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
+modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
+modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
+modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
+modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
+modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
+modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
+modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
+modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
+modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
+modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
+modernc.org/libc v1.21.2/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
+modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
+modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
+modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
+modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
+modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
+modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
+modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs=
+modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/internal/ali_cnrid/types.go b/internal/ali_cnrid/types.go
deleted file mode 100644
index 9aa6f39..0000000
--- a/internal/ali_cnrid/types.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package kyc
-
-type KycAli struct {
- ParamType string `json:"param_type"`
- IdentifyNum string `json:"identify_num"`
- UserName string `json:"user_name"`
-}
diff --git a/internal/authcode/authcode_test.go b/internal/authcode/authcode_test.go
new file mode 100644
index 0000000..b9cf7de
--- /dev/null
+++ b/internal/authcode/authcode_test.go
@@ -0,0 +1,81 @@
+package authcode
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/testutil"
+)
+
+func TestNewAuthCode(t *testing.T) {
+ mr := testutil.Setup(t)
+ _ = mr
+
+ ctx := context.Background()
+ code, err := NewAuthCode(ctx, "client-1", "user@example.com")
+ require.NoError(t, err)
+ assert.NotEmpty(t, code)
+}
+
+func TestVerifyAuthCodeValid(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ code, err := NewAuthCode(ctx, "client-1", "user@example.com")
+ require.NoError(t, err)
+
+ token, ok := VerifyAuthCode(ctx, code)
+ require.True(t, ok)
+ assert.Equal(t, "client-1", token.ClientId)
+ assert.Equal(t, "user@example.com", token.Email)
+}
+
+func TestVerifyAuthCodeOneTimeUse(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ code, err := NewAuthCode(ctx, "client-1", "user@example.com")
+ require.NoError(t, err)
+
+ // First use – should succeed
+ _, ok := VerifyAuthCode(ctx, code)
+ require.True(t, ok)
+
+ // Second use – must fail (one-time)
+ _, ok = VerifyAuthCode(ctx, code)
+ assert.False(t, ok)
+}
+
+func TestVerifyAuthCodeInvalidCode(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ _, ok := VerifyAuthCode(ctx, "completely-wrong-code")
+ assert.False(t, ok)
+}
+
+func TestVerifyAuthCodeExpired(t *testing.T) {
+ mr := testutil.Setup(t)
+
+ ctx := context.Background()
+ code, err := NewAuthCode(ctx, "client-1", "user@example.com")
+ require.NoError(t, err)
+
+ // Fast-forward time past the TTL (10 minutes from testutil)
+ mr.FastForward(11 * time.Minute)
+
+ _, ok := VerifyAuthCode(ctx, code)
+ assert.False(t, ok, "expired code should not be valid")
+}
+
+func TestNewAuthCodeUniqueness(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ c1, _ := NewAuthCode(ctx, "client", "a@example.com")
+ c2, _ := NewAuthCode(ctx, "client", "a@example.com")
+ assert.NotEqual(t, c1, c2)
+}
diff --git a/internal/authtoken/authtoken.go b/internal/authtoken/authtoken.go
index be97e98..9ace3c6 100644
--- a/internal/authtoken/authtoken.go
+++ b/internal/authtoken/authtoken.go
@@ -243,7 +243,7 @@ func (self *Token) RevokeRefreshToken(ctx context.Context, refreshToken string)
func (self *Token) HeaderVerify(ctx context.Context, header string) (string, error) {
if header == "" {
- return "", nil
+ return "", errors.New("[Auth Token] missing Authorization header")
}
// Split header to 2
diff --git a/internal/authtoken/authtoken_test.go b/internal/authtoken/authtoken_test.go
new file mode 100644
index 0000000..6e822c4
--- /dev/null
+++ b/internal/authtoken/authtoken_test.go
@@ -0,0 +1,207 @@
+package authtoken
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func makeToken() Token {
+ return Token{Application: "test-app"}
+}
+
+// seedClient persists a client row whose encrypted secret decrypts to the
+// plaintext given by testutil.TestClientSecret.
+func seedTestClient(t *testing.T) {
+ t.Helper()
+ testutil.SeedClient(t)
+}
+
+func TestGenerateRefreshToken(t *testing.T) {
+ tok := makeToken()
+ r1, err := tok.GenerateRefreshToken()
+ require.NoError(t, err)
+ assert.NotEmpty(t, r1)
+
+ r2, err := tok.GenerateRefreshToken()
+ require.NoError(t, err)
+ assert.NotEqual(t, r1, r2, "refresh tokens must be unique")
+}
+
+func TestIssueTokens(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ access, refresh, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+ assert.NotEmpty(t, access)
+ assert.NotEmpty(t, refresh)
+}
+
+func TestRefreshAccessToken(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ _, refresh, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+
+ newAccess, err := tok.RefreshAccessToken(ctx, refresh)
+ require.NoError(t, err)
+ assert.NotEmpty(t, newAccess)
+}
+
+func TestRefreshAccessTokenInvalid(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ tok := makeToken()
+
+ _, err := tok.RefreshAccessToken(ctx, "invalid-refresh-token")
+ require.Error(t, err)
+}
+
+func TestRenewRefreshToken(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ _, old, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+
+ newRefresh, err := tok.RenewRefreshToken(ctx, old)
+ require.NoError(t, err)
+ assert.NotEmpty(t, newRefresh)
+ assert.NotEqual(t, old, newRefresh)
+
+ // Old token should no longer be valid
+ _, err = tok.RefreshAccessToken(ctx, old)
+ require.Error(t, err)
+}
+
+func TestRevokeRefreshToken(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ _, refresh, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+
+ require.NoError(t, tok.RevokeRefreshToken(ctx, refresh))
+
+ _, err = tok.RefreshAccessToken(ctx, refresh)
+ require.Error(t, err)
+}
+
+func TestRevokeNonExistentTokenNoError(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ tok := makeToken()
+ // should not fail even if token does not exist
+ assert.NoError(t, tok.RevokeRefreshToken(ctx, "does-not-exist"))
+}
+
+func TestHeaderVerifyValid(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ access, _, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+
+ uid, err := tok.HeaderVerify(ctx, "Bearer "+access)
+ require.NoError(t, err)
+ assert.Equal(t, userId.String(), uid)
+}
+
+func TestHeaderVerifyEmpty(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ tok := makeToken()
+
+ uid, err := tok.HeaderVerify(ctx, "")
+ require.Error(t, err)
+ assert.Empty(t, uid)
+}
+
+func TestHeaderVerifyMalformed(t *testing.T) {
+ testutil.Setup(t)
+
+ ctx := context.Background()
+ tok := makeToken()
+
+ _, err := tok.HeaderVerify(ctx, "NotBearer token")
+ require.Error(t, err)
+}
+
+func TestHeaderVerifyExpiredToken(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ // Use a very short access TTL
+ viper.Set("ttl.access_ttl", -1*time.Second)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ access, _, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+
+ _, err = tok.HeaderVerify(ctx, "Bearer "+access)
+ require.Error(t, err)
+}
+
+func TestNewClaims(t *testing.T) {
+ tok := makeToken()
+ userId := uuid.New()
+ claims := tok.NewClaims(testutil.TestClientID, userId)
+
+ assert.Equal(t, testutil.TestClientID, claims.ClientId)
+ assert.Equal(t, userId, claims.UserID)
+ assert.Equal(t, "test-app", claims.Issuer)
+}
+
+// Verify that IssueTokens registers the refresh token in the user and
+// client index sets in Redis.
+func TestIssueTokensRedisIndex(t *testing.T) {
+ testutil.Setup(t)
+ seedTestClient(t)
+
+ ctx := context.Background()
+ userId := uuid.New()
+ tok := makeToken()
+
+ _, refresh, err := tok.IssueTokens(ctx, testutil.TestClientID, userId)
+ require.NoError(t, err)
+
+ userSetKey := "user:" + userId.String() + ":refresh_tokens"
+ isMember, err := data.Redis.SIsMember(ctx, userSetKey, refresh).Result()
+ require.NoError(t, err)
+ assert.True(t, isMember)
+}
diff --git a/internal/cryptography/aes.go b/internal/cryptography/aes.go
index 7e7cdb0..6f74135 100644
--- a/internal/cryptography/aes.go
+++ b/internal/cryptography/aes.go
@@ -149,6 +149,10 @@ func AESCBCDecrypt(encoded string, key []byte) ([]byte, error) {
iv := data[:block.BlockSize()]
data = data[block.BlockSize():]
+ if len(data)%block.BlockSize() != 0 {
+ return nil, errors.New("[Cryptography AES] ciphertext is not block-aligned")
+ }
+
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(data, data)
diff --git a/internal/cryptography/aes_test.go b/internal/cryptography/aes_test.go
new file mode 100644
index 0000000..b562d27
--- /dev/null
+++ b/internal/cryptography/aes_test.go
@@ -0,0 +1,164 @@
+package cryptography
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ key16 = []byte("1234567890123456")
+ key24 = []byte("123456789012345678901234")
+ key32 = []byte("12345678901234567890123456789012")
+)
+
+// ---- AES-GCM ----
+
+func TestAESGCMEncryptDecryptRoundTrip(t *testing.T) {
+ for _, key := range [][]byte{key16, key24, key32} {
+ plaintext := []byte("hello, world")
+ ciphertext, err := AESGCMEncrypt(plaintext, key)
+ require.NoError(t, err)
+ require.NotEmpty(t, ciphertext)
+
+ got, err := AESGCMDecrypt(ciphertext, key)
+ require.NoError(t, err)
+ assert.Equal(t, plaintext, got)
+ }
+}
+
+func TestAESGCMEncryptProducesUniqueOutput(t *testing.T) {
+ plaintext := []byte("same input")
+ c1, _ := AESGCMEncrypt(plaintext, key32)
+ c2, _ := AESGCMEncrypt(plaintext, key32)
+ // nonces differ, so ciphertexts must differ
+ assert.NotEqual(t, c1, c2)
+}
+
+func TestAESGCMEncryptBadKeyLength(t *testing.T) {
+ _, err := AESGCMEncrypt([]byte("data"), []byte("shortkey"))
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "AES key length")
+}
+
+func TestAESGCMDecryptBadKeyLength(t *testing.T) {
+ _, err := AESGCMDecrypt("somedata", []byte("bad"))
+ require.Error(t, err)
+}
+
+func TestAESGCMDecryptInvalidBase64(t *testing.T) {
+ _, err := AESGCMDecrypt("!!!not-base64!!!", key32)
+ require.Error(t, err)
+}
+
+func TestAESGCMDecryptTooShort(t *testing.T) {
+ // A valid base64 string that is too short to contain nonce
+ _, err := AESGCMDecrypt("YQ", key32) // "a" – only 1 byte
+ require.Error(t, err)
+}
+
+// ---- AES-CBC ----
+
+func TestAESCBCEncryptDecryptRoundTrip(t *testing.T) {
+ for _, key := range [][]byte{key16, key24, key32} {
+ plaintext := []byte("CBC round-trip test")
+ ciphertext, err := AESCBCEncrypt(plaintext, key)
+ require.NoError(t, err)
+ require.NotEmpty(t, ciphertext)
+
+ got, err := AESCBCDecrypt(ciphertext, key)
+ require.NoError(t, err)
+ assert.Equal(t, plaintext, got)
+ }
+}
+
+func TestAESCBCEncryptEmptyPlaintext(t *testing.T) {
+ ciphertext, err := AESCBCEncrypt([]byte{}, key32)
+ require.NoError(t, err)
+ got, err := AESCBCDecrypt(ciphertext, key32)
+ require.NoError(t, err)
+ assert.Equal(t, []byte{}, got)
+}
+
+func TestAESCBCEncryptBadKeyLength(t *testing.T) {
+ _, err := AESCBCEncrypt([]byte("data"), []byte("bad-key"))
+ require.Error(t, err)
+}
+
+func TestAESCBCDecryptInvalidBase64(t *testing.T) {
+ _, err := AESCBCDecrypt("!!!!", key32)
+ require.Error(t, err)
+}
+
+func TestAESCBCDecryptTooShort(t *testing.T) {
+ // base64 of a single byte – shorter than block size
+ _, err := AESCBCDecrypt("YQ", key32)
+ require.Error(t, err)
+}
+
+// ---- AES-CFB ----
+
+func TestAESCFBEncryptDecryptRoundTrip(t *testing.T) {
+ for _, key := range [][]byte{key16, key24, key32} {
+ original := []byte("CFB mode test data")
+ // AESCFBEncrypt modifies plaintext in-place; keep original copy for assertion
+ plaintext := append([]byte(nil), original...)
+ ciphertext, err := AESCFBEncrypt(plaintext, key)
+ require.NoError(t, err)
+ require.NotEmpty(t, ciphertext)
+
+ got, err := AESCFBDecrypt(ciphertext, key)
+ require.NoError(t, err)
+ assert.Equal(t, original, got)
+ }
+}
+
+func TestAESCFBEncryptBadKeyLength(t *testing.T) {
+ _, err := AESCFBEncrypt([]byte("data"), []byte("x"))
+ require.Error(t, err)
+}
+
+func TestAESCFBDecryptTooShort(t *testing.T) {
+ _, err := AESCFBDecrypt("YQ", key32)
+ require.Error(t, err)
+}
+
+// ---- PKCS7 padding ----
+
+func TestPkcs7PadUnpad(t *testing.T) {
+ data := []byte("hello")
+ padded := pkcs7Pad(data, 16)
+ assert.Equal(t, 16, len(padded))
+
+ unpadded, err := pkcs7Unpad(padded)
+ require.NoError(t, err)
+ assert.Equal(t, data, unpadded)
+}
+
+func TestPkcs7UnpadEmpty(t *testing.T) {
+ _, err := pkcs7Unpad([]byte{})
+ require.Error(t, err)
+}
+
+func TestPkcs7UnpadInvalidPadding(t *testing.T) {
+ // last byte claims padding of 0 – invalid
+ _, err := pkcs7Unpad([]byte{0x01, 0x02, 0x00})
+ require.Error(t, err)
+}
+
+// ---- normalizeKey ----
+
+func TestNormalizeKeyValidLengths(t *testing.T) {
+ for _, k := range [][]byte{key16, key24, key32} {
+ got, err := normalizeKey(k)
+ require.NoError(t, err)
+ assert.Equal(t, k, got)
+ }
+}
+
+func TestNormalizeKeyInvalidLength(t *testing.T) {
+ _, err := normalizeKey([]byte(strings.Repeat("x", 10)))
+ require.Error(t, err)
+}
diff --git a/internal/cryptography/base64_test.go b/internal/cryptography/base64_test.go
new file mode 100644
index 0000000..5cbea10
--- /dev/null
+++ b/internal/cryptography/base64_test.go
@@ -0,0 +1,47 @@
+package cryptography
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsBase64StdValid(t *testing.T) {
+ cases := []struct {
+ name string
+ input string
+ }{
+ {"non-empty", base64.StdEncoding.EncodeToString([]byte("hello"))},
+ {"longer", base64.StdEncoding.EncodeToString([]byte("hello, world!"))},
+ // Note: empty string encodes to "" which IsBase64Std("") returns false (early exit)
+ }
+ for _, tc := range cases {
+ assert.True(t, IsBase64Std(tc.input), "expected %q to be valid base64", tc.input)
+ }
+}
+
+func TestIsBase64StdEmptyString(t *testing.T) {
+ assert.False(t, IsBase64Std(""))
+}
+
+func TestIsBase64StdInvalidChars(t *testing.T) {
+ assert.False(t, IsBase64Std("not-base64!!!"))
+}
+
+func TestIsBase64StdWrongPadding(t *testing.T) {
+ // "aGVsbG8" is base64url without padding – standard decoder requires padding
+ assert.False(t, IsBase64Std("aGVsbG8"))
+}
+
+func TestIsBase64StdWhitespaceOnly(t *testing.T) {
+ // " " → TrimSpace → "" → len % 4 == 0 → attempts decode → succeeds → true
+ // IsBase64Std trims leading/trailing whitespace, so all-whitespace passes
+ // because empty string decodes without error. Document actual behaviour:
+ assert.True(t, IsBase64Std(" "))
+}
+
+func TestIsBase64StdPaddedBase64(t *testing.T) {
+ // Standard base64 with padding
+ assert.True(t, IsBase64Std("aGVsbG8="))
+}
diff --git a/internal/cryptography/bcrypt_test.go b/internal/cryptography/bcrypt_test.go
new file mode 100644
index 0000000..996a829
--- /dev/null
+++ b/internal/cryptography/bcrypt_test.go
@@ -0,0 +1,36 @@
+package cryptography
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBcryptHashAndVerify(t *testing.T) {
+ input := "super-secret-password"
+ hash, err := BcryptHash(input)
+ require.NoError(t, err)
+ require.NotEmpty(t, hash)
+ assert.NotEqual(t, input, hash)
+ assert.True(t, BcryptVerify(input, hash))
+}
+
+func TestBcryptVerifyWrongPassword(t *testing.T) {
+ hash, err := BcryptHash("correct-password")
+ require.NoError(t, err)
+ assert.False(t, BcryptVerify("wrong-password", hash))
+}
+
+func TestBcryptHashUniqueness(t *testing.T) {
+ h1, _ := BcryptHash("same")
+ h2, _ := BcryptHash("same")
+ // bcrypt uses a random salt – hashes must differ
+ assert.NotEqual(t, h1, h2)
+}
+
+func TestBcryptHashEmptyInput(t *testing.T) {
+ hash, err := BcryptHash("")
+ require.NoError(t, err)
+ assert.True(t, BcryptVerify("", hash))
+}
diff --git a/internal/email/email_test.go b/internal/email/email_test.go
new file mode 100644
index 0000000..9ef02e4
--- /dev/null
+++ b/internal/email/email_test.go
@@ -0,0 +1,94 @@
+package email
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func setupEmailViper(host, user, pass, security string, port int) {
+ viper.Reset()
+ viper.Set("email.host", host)
+ viper.Set("email.port", port)
+ viper.Set("email.username", user)
+ viper.Set("email.password", pass)
+ viper.Set("email.security", security)
+ viper.Set("email.insecure_skip_verify", false)
+}
+
+func TestNewSMTPClientMissingHost(t *testing.T) {
+ viper.Reset()
+ defer viper.Reset()
+
+ _, err := new(Client).NewSMTPClient()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "SMTP config not set")
+}
+
+func TestNewSMTPClientMissingPassword(t *testing.T) {
+ setupEmailViper("smtp.example.com", "user@example.com", "", "plain", 587)
+ defer viper.Reset()
+
+ _, err := new(Client).NewSMTPClient()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "SMTP basic auth requires email.password")
+}
+
+func TestNewSMTPClientUnknownSecurity(t *testing.T) {
+ setupEmailViper("smtp.example.com", "user@example.com", "pass", "tls13", 587)
+ defer viper.Reset()
+
+ _, err := new(Client).NewSMTPClient()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "unknown smtp security mode")
+}
+
+func TestNewSMTPClientSSL(t *testing.T) {
+ setupEmailViper("smtp.example.com", "user@example.com", "pass", "ssl", 465)
+ defer viper.Reset()
+
+ c, err := new(Client).NewSMTPClient()
+ require.NoError(t, err)
+ assert.Equal(t, "ssl", c.security)
+ assert.True(t, c.dialer.SSL)
+}
+
+func TestNewSMTPClientStartTLS(t *testing.T) {
+ setupEmailViper("smtp.example.com", "user@example.com", "pass", "starttls", 587)
+ defer viper.Reset()
+
+ c, err := new(Client).NewSMTPClient()
+ require.NoError(t, err)
+ assert.Equal(t, "starttls", c.security)
+ assert.False(t, c.dialer.SSL)
+ assert.NotNil(t, c.dialer.TLSConfig)
+}
+
+func TestNewSMTPClientPlain(t *testing.T) {
+ setupEmailViper("smtp.example.com", "user@example.com", "pass", "plain", 25)
+ defer viper.Reset()
+
+ c, err := new(Client).NewSMTPClient()
+ require.NoError(t, err)
+ assert.Equal(t, "plain", c.security)
+ assert.False(t, c.dialer.SSL)
+ assert.Nil(t, c.dialer.TLSConfig)
+}
+
+func TestNewSMTPClientEmptySecurity(t *testing.T) {
+ setupEmailViper("smtp.example.com", "user@example.com", "pass", "", 25)
+ defer viper.Reset()
+
+ c, err := new(Client).NewSMTPClient()
+ require.NoError(t, err)
+ assert.Nil(t, c.dialer.TLSConfig)
+}
+
+func TestSendUninitializedDialer(t *testing.T) {
+ c := &Client{}
+ _, err := c.Send("from@test.com", "to@test.com", "subject", "html
")
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "SMTP dialer not initialized")
+}
diff --git a/internal/exception/builder.go b/internal/exception/builder.go
index 8390606..42ecc96 100644
--- a/internal/exception/builder.go
+++ b/internal/exception/builder.go
@@ -3,70 +3,139 @@ package exception
import (
"context"
"fmt"
+ "nixcn-cms/tracer"
+ "runtime"
+
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/codes"
+ "go.opentelemetry.io/otel/trace"
)
-// 12 chars len
+// context passthrough
+type ctxKey string
+
+const (
+ ctxKeyService ctxKey = "exception_service"
+ ctxKeyEndpoint ctxKey = "exception_endpoint"
+)
+
+func ContextWithService(ctx context.Context, service string) context.Context {
+ return context.WithValue(ctx, ctxKeyService, service)
+}
+
+func ContextWithEndpoint(ctx context.Context, endpoint string) context.Context {
+ return context.WithValue(ctx, ctxKeyEndpoint, endpoint)
+}
+
+func extractFromCtx(ctx context.Context, key ctxKey, fallback string) string {
+ if val, ok := ctx.Value(key).(string); ok && val != "" {
+ return val
+ }
+ return fallback
+}
+
+// 13 chars len
// :1=status
+// :3=endpoint
// :3=service
-// :2=endpoint
// :1=common/specific
// :5=original
-
type Builder struct {
Status string
- Service string
Endpoint string
+ Service string
Type string
Original string
Error error
+ Span trace.Span
ErrorCode string
}
-func (self *Builder) SetStatus(s string) *Builder {
- self.Status = s
- return self
+// functional option
+type Option func(*Builder)
+
+func New(opts ...Option) *Builder {
+ b := &Builder{}
+ for _, opt := range opts {
+ opt(b)
+ }
+ return b
}
-func (self *Builder) SetService(s string) *Builder {
- self.Service = s
- return self
+func WithStatus(s string) Option {
+ return func(b *Builder) {
+ b.Status = s
+ }
}
-func (self *Builder) SetEndpoint(s string) *Builder {
- self.Endpoint = s
- return self
+func WithType(s string) Option {
+ return func(b *Builder) {
+ b.Type = s
+ }
}
-func (self *Builder) SetType(s string) *Builder {
- self.Type = s
- return self
+func WithOriginal(s string) Option {
+ return func(b *Builder) {
+ b.Original = s
+ }
}
-func (self *Builder) SetOriginal(s string) *Builder {
- self.Original = s
- return self
+func WithError(e error) Option {
+ return func(b *Builder) {
+ b.Error = e
+ }
}
-func (self *Builder) SetError(e error) *Builder {
- self.Error = e
- return self
+func WithSpan(s trace.Span) Option {
+ return func(b *Builder) {
+ b.Span = s
+ }
}
+// exception builder
func (self *Builder) build() {
self.ErrorCode = fmt.Sprintf("%s%s%s%s%s",
self.Status,
- self.Service,
self.Endpoint,
+ self.Service,
self.Type,
self.Original,
)
}
func (self *Builder) Throw(ctx context.Context) *Builder {
+ self.Service = extractFromCtx(ctx, ctxKeyService, "svc")
+ self.Endpoint = extractFromCtx(ctx, ctxKeyEndpoint, "ep")
+
self.build()
- if self.Error != nil {
+
+ pc, file, line, _ := runtime.Caller(1)
+ function := runtime.FuncForPC(pc)
+
+ if self.Span == nil {
+ self.Span = trace.SpanFromContext(ctx)
+ }
+
+ if self.Original != CommonSuccess {
+ if self.Span != nil && self.Span.IsRecording() {
+ self.Span.SetAttributes(
+ attribute.String("code.function", function.Name()),
+ attribute.String("code.file", tracer.ShortPath(file)),
+ attribute.Int("code.line", line),
+ attribute.String("exception.code", self.ErrorCode),
+ )
+
+ if self.Error != nil {
+ self.Span.RecordError(self.Error)
+ self.Span.SetStatus(codes.Error, self.Error.Error())
+ } else {
+ self.Span.SetStatus(codes.Error, fmt.Sprintf("ServiceException: %s", self.ErrorCode))
+ }
+ }
+
ErrorHandler(ctx, self.Status, self.ErrorCode, self.Error)
}
+
return self
}
diff --git a/internal/exception/builder_test.go b/internal/exception/builder_test.go
new file mode 100644
index 0000000..94a310e
--- /dev/null
+++ b/internal/exception/builder_test.go
@@ -0,0 +1,135 @@
+package exception
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestContextWithServiceAndEndpoint(t *testing.T) {
+ ctx := context.Background()
+ ctx = ContextWithService(ctx, "my_service")
+ ctx = ContextWithEndpoint(ctx, "my_endpoint")
+
+ svc := extractFromCtx(ctx, ctxKeyService, "fallback")
+ ep := extractFromCtx(ctx, ctxKeyEndpoint, "fallback")
+
+ assert.Equal(t, "my_service", svc)
+ assert.Equal(t, "my_endpoint", ep)
+}
+
+func TestExtractFromCtxFallback(t *testing.T) {
+ ctx := context.Background()
+ val := extractFromCtx(ctx, ctxKeyService, "default")
+ assert.Equal(t, "default", val)
+}
+
+func TestBuilderString(t *testing.T) {
+ // Use proper 3-char endpoint and service to form a 13-char code
+ ctx := ContextWithService(context.Background(), ServiceAuthToken)
+ ctx = ContextWithEndpoint(ctx, EndpointAuthToken)
+
+ b := New(
+ WithStatus(StatusSuccess),
+ WithType(TypeCommon),
+ WithOriginal(CommonSuccess),
+ ).Throw(ctx)
+
+ s := b.String()
+ require.Len(t, s, 13, "error code must be 13 chars")
+ assert.Equal(t, StatusSuccess, string(s[0]))
+}
+
+func TestBuilderThrowSuccess(t *testing.T) {
+ ctx := ContextWithService(context.Background(), ServiceAuthToken)
+ ctx = ContextWithEndpoint(ctx, EndpointAuthToken)
+
+ b := New(
+ WithStatus(StatusSuccess),
+ WithType(TypeCommon),
+ WithOriginal(CommonSuccess),
+ ).Throw(ctx)
+
+ assert.Equal(t, CommonSuccess, b.Original)
+ assert.NotEmpty(t, b.ErrorCode)
+}
+
+func TestBuilderThrowWithError(t *testing.T) {
+ ctx := ContextWithService(context.Background(), ServiceAuthToken)
+ ctx = ContextWithEndpoint(ctx, EndpointAuthToken)
+
+ origErr := errors.New("something went wrong")
+ b := New(
+ WithStatus(StatusUser),
+ WithType(TypeSpecific),
+ WithOriginal(CommonErrorUnauthorized),
+ WithError(origErr),
+ ).Throw(ctx)
+
+ assert.Equal(t, origErr, b.Error)
+ assert.Equal(t, StatusUser, b.Status)
+}
+
+func TestBuilderWithOptionsApplied(t *testing.T) {
+ err := errors.New("test")
+ b := New(
+ WithStatus(StatusServer),
+ WithType(TypeSpecific),
+ WithOriginal(CommonErrorInternal),
+ WithError(err),
+ )
+
+ assert.Equal(t, StatusServer, b.Status)
+ assert.Equal(t, TypeSpecific, b.Type)
+ assert.Equal(t, CommonErrorInternal, b.Original)
+ assert.Equal(t, err, b.Error)
+}
+
+func TestBuilderErrorCodeLength(t *testing.T) {
+ ctx := ContextWithService(context.Background(), "svc")
+ ctx = ContextWithEndpoint(ctx, "ep_")
+
+ b := New(
+ WithStatus(StatusSuccess),
+ WithType(TypeCommon),
+ WithOriginal(CommonSuccess),
+ ).Throw(ctx)
+
+ // Status(1) + Endpoint(3) + Service(3) + Type(1) + Original(5) = 13
+ assert.Len(t, b.ErrorCode, 13)
+}
+
+func TestErrorHandlerDoesNotPanic(t *testing.T) {
+ ctx := context.Background()
+ testErr := errors.New("test error")
+
+ for _, status := range []string{StatusSuccess, StatusUser, StatusServer, StatusClient} {
+ s := status
+ assert.NotPanics(t, func() {
+ ErrorHandler(ctx, s, "code12345678", testErr)
+ }, "ErrorHandler must not panic for status %q", s)
+ }
+}
+
+func TestErrorHandlerNilError(t *testing.T) {
+ ctx := context.Background()
+ assert.NotPanics(t, func() {
+ ErrorHandler(ctx, StatusUser, "code12345678", nil)
+ })
+}
+
+func TestBuilderNilThrow(t *testing.T) {
+ b := New(
+ WithStatus(StatusSuccess),
+ WithType(TypeCommon),
+ WithOriginal(CommonSuccess),
+ )
+ // Throw on empty context (no service/endpoint set) must not panic
+ assert.NotPanics(t, func() {
+ thrown := b.Throw(context.Background())
+ assert.Equal(t, CommonSuccess, thrown.Original)
+ })
+}
diff --git a/internal/exception/error.go b/internal/exception/error.go
index c7ab181..23b5d7f 100644
--- a/internal/exception/error.go
+++ b/internal/exception/error.go
@@ -8,12 +8,12 @@ import (
func ErrorHandler(ctx context.Context, status string, errorCode string, err error) {
switch status {
case StatusSuccess:
- slog.InfoContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
+ slog.InfoContext(ctx, "Service exception!, ErrorCode: "+errorCode, slog.String("error_code", errorCode), slog.Any("error", err))
case StatusUser:
- slog.WarnContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
+ slog.WarnContext(ctx, "Service exception!, ErrorCode: "+errorCode, slog.String("error_code", errorCode), slog.Any("error", err))
case StatusServer:
- slog.ErrorContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
+ slog.ErrorContext(ctx, "Service exception!, ErrorCode: "+errorCode, slog.String("error_code", errorCode), slog.Any("error", err))
case StatusClient:
- slog.ErrorContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
+ slog.ErrorContext(ctx, "Service exception!, ErrorCode: "+errorCode, slog.String("error_code", errorCode), slog.Any("error", err))
}
}
diff --git a/internal/ali_cnrid/kyc.go b/internal/kyc/cnrid.go
similarity index 71%
rename from internal/ali_cnrid/kyc.go
rename to internal/kyc/cnrid.go
index 228ca16..cb8f0c9 100644
--- a/internal/ali_cnrid/kyc.go
+++ b/internal/kyc/cnrid.go
@@ -2,12 +2,8 @@ package kyc
import (
"crypto/md5"
- "encoding/base64"
"encoding/hex"
- "encoding/json"
- "errors"
"fmt"
- "nixcn-cms/internal/cryptography"
"unicode/utf8"
alicloudauth20190307 "github.com/alibabacloud-go/cloudauth-20190307/v4/client"
@@ -18,55 +14,7 @@ import (
"github.com/spf13/viper"
)
-func DecodeB64Json(b64Json string) (*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 {
- return nil, errors.New("[KYC] invalid json structure")
- }
-
- return &kyc, nil
-}
-
-func EncodeAES(kyc *KycInfo) (*string, error) {
- plainJson, err := json.Marshal(kyc)
- if err != nil {
- return nil, err
- }
-
- aesKey := viper.GetString("secrets.kyc_info_key")
- encrypted, err := cryptography.AESCBCEncrypt(plainJson, []byte(aesKey))
- if err != nil {
- return nil, err
- }
-
- return &encrypted, nil
-}
-
-func DecodeAES(cipherStr string) (*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 {
- return nil, errors.New("[KYC] invalid decrypted json")
- }
-
- return &kyc, nil
-}
-
-func MD5AliEnc(kyc *KycInfo) (*KycAli, error) {
- if kyc.Type != "Chinese" {
- return nil, nil
- }
-
+func CNRidMD5AliEnc(kyc *CNRidInfo) (*AliCloudAuth, error) {
// MD5 Legal Name rule: First Chinese char md5enc, remaining plain, at least 2 Chinese chars
if len(kyc.LegalName) < 2 || utf8.RuneCountInString(kyc.LegalName) < 2 {
return nil, fmt.Errorf("input string must have at least 2 Chinese characters")
@@ -101,7 +49,7 @@ func MD5AliEnc(kyc *KycInfo) (*KycAli, error) {
rid := ridPrefix + ridMiddleHash + ridSuffix
// Aliyun Id2MetaVerify API Params
- var kycAli KycAli
+ var kycAli AliCloudAuth
kycAli.ParamType = "md5"
kycAli.UserName = ln
kycAli.IdentifyNum = rid
@@ -109,7 +57,7 @@ func MD5AliEnc(kyc *KycInfo) (*KycAli, error) {
return &kycAli, nil
}
-func AliId2MetaVerify(kycAli *KycAli) (*string, error) {
+func AliId2MetaVerify(kycAli *AliCloudAuth) (*string, error) {
// Create aliyun openapi credential
credentialConfig := new(alicredential.Config).
SetType("access_key").
diff --git a/internal/kyc/cnrid_test.go b/internal/kyc/cnrid_test.go
new file mode 100644
index 0000000..506c42c
--- /dev/null
+++ b/internal/kyc/cnrid_test.go
@@ -0,0 +1,65 @@
+package kyc
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCNRidMD5AliEncValid(t *testing.T) {
+ kyc := &CNRidInfo{
+ LegalName: "张三",
+ ResidentId: "110101199001011234",
+ }
+ result, err := CNRidMD5AliEnc(kyc)
+ require.NoError(t, err)
+ require.NotNil(t, result)
+
+ assert.Equal(t, "md5", result.ParamType)
+ // First Chinese char (张) should be MD5-hashed, remaining "三" stays plain.
+ // Verify the result contains the remaining character(s).
+ assert.True(t, strings.HasSuffix(result.UserName, "三"),
+ "UserName should end with remaining name chars, got %q", result.UserName)
+
+ // ResidentId: first 6 plain + middle md5 + last 4 plain
+ assert.True(t, strings.HasPrefix(result.IdentifyNum, "110101"),
+ "IdentifyNum should start with first 6 digits, got %q", result.IdentifyNum)
+ assert.True(t, strings.HasSuffix(result.IdentifyNum, "1234"),
+ "IdentifyNum should end with last 4 digits, got %q", result.IdentifyNum)
+}
+
+func TestCNRidMD5AliEncShortName(t *testing.T) {
+ kyc := &CNRidInfo{
+ LegalName: "张", // only 1 char – should fail
+ ResidentId: "110101199001011234",
+ }
+ _, err := CNRidMD5AliEnc(kyc)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "2 Chinese characters")
+}
+
+func TestCNRidMD5AliEncShortResidentId(t *testing.T) {
+ kyc := &CNRidInfo{
+ LegalName: "张三",
+ ResidentId: "12345", // too short
+ }
+ _, err := CNRidMD5AliEnc(kyc)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "18 characters")
+}
+
+func TestCNRidMD5AliEncDeterministicMiddleHash(t *testing.T) {
+ kyc := &CNRidInfo{
+ LegalName: "李四",
+ ResidentId: "110101199001011234",
+ }
+ r1, err1 := CNRidMD5AliEnc(kyc)
+ r2, err2 := CNRidMD5AliEnc(kyc)
+ require.NoError(t, err1)
+ require.NoError(t, err2)
+ // MD5 is deterministic
+ assert.Equal(t, r1.IdentifyNum, r2.IdentifyNum)
+ assert.Equal(t, r1.UserName, r2.UserName)
+}
diff --git a/internal/kyc/crypto.go b/internal/kyc/crypto.go
new file mode 100644
index 0000000..19cf0b1
--- /dev/null
+++ b/internal/kyc/crypto.go
@@ -0,0 +1,39 @@
+package kyc
+
+import (
+ "encoding/json"
+ "errors"
+ "nixcn-cms/internal/cryptography"
+
+ "github.com/spf13/viper"
+)
+
+func EncodeAES(kyc any) (*string, error) {
+ plainJson, err := json.Marshal(kyc)
+ if err != nil {
+ return nil, err
+ }
+
+ aesKey := viper.GetString("secrets.kyc_info_key")
+ encrypted, err := cryptography.AESCBCEncrypt(plainJson, []byte(aesKey))
+ if err != nil {
+ return nil, err
+ }
+
+ return &encrypted, nil
+}
+
+func DecodeAES(cipherStr string) (any, error) {
+ aesKey := viper.GetString("secrets.kyc_info_key")
+ plainBytes, err := cryptography.AESCBCDecrypt(cipherStr, []byte(aesKey))
+ if err != nil {
+ return nil, err
+ }
+
+ var kycInfo any
+ if err := json.Unmarshal(plainBytes, &kycInfo); err != nil {
+ return nil, errors.New("[KYC] invalid decrypted json")
+ }
+
+ return &kycInfo, nil
+}
diff --git a/internal/kyc/crypto_test.go b/internal/kyc/crypto_test.go
new file mode 100644
index 0000000..6915bfa
--- /dev/null
+++ b/internal/kyc/crypto_test.go
@@ -0,0 +1,100 @@
+package kyc
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const testKYCAESKey = "testkyckey123456" // 16 bytes
+
+func setupKYCViper(t *testing.T) {
+ t.Helper()
+ viper.Set("secrets.kyc_info_key", testKYCAESKey)
+ t.Cleanup(func() { viper.Reset() })
+}
+
+// ---- EncodeAES ----
+
+func TestEncodeAESRoundTrip(t *testing.T) {
+ setupKYCViper(t)
+
+ original := &CNRidInfo{LegalName: "张三", ResidentId: "110101199001011234"}
+ encoded, err := EncodeAES(original)
+ require.NoError(t, err)
+ require.NotNil(t, encoded)
+ assert.NotEmpty(t, *encoded)
+}
+
+func TestEncodeAESDeterministic(t *testing.T) {
+ setupKYCViper(t)
+
+ kyc := &PassportInfo{ID: "abc-123"}
+ r1, err1 := EncodeAES(kyc)
+ r2, err2 := EncodeAES(kyc)
+ require.NoError(t, err1)
+ require.NoError(t, err2)
+ // AES-CBC with a fresh IV each call — ciphertexts differ but both must
+ // decode back to the same plaintext (tested in round-trip below).
+ require.NotNil(t, r1)
+ require.NotNil(t, r2)
+}
+
+func TestEncodeAESWrongKeyLength(t *testing.T) {
+ viper.Set("secrets.kyc_info_key", "short") // not 16/24/32 bytes
+ t.Cleanup(func() { viper.Reset() })
+
+ _, err := EncodeAES(&CNRidInfo{LegalName: "张三", ResidentId: "110101199001011234"})
+ assert.Error(t, err)
+}
+
+// ---- DecodeAES ----
+
+func TestDecodeAESRoundTrip(t *testing.T) {
+ setupKYCViper(t)
+
+ original := &CNRidInfo{LegalName: "李四", ResidentId: "310101199901015678"}
+ encoded, err := EncodeAES(original)
+ require.NoError(t, err)
+
+ decoded, err := DecodeAES(*encoded)
+ require.NoError(t, err)
+ require.NotNil(t, decoded)
+}
+
+func TestDecodeAESInvalidBase64(t *testing.T) {
+ setupKYCViper(t)
+
+ // "@@@" contains characters that are not valid base64 — must return error.
+ _, err := DecodeAES("@@@")
+ assert.Error(t, err)
+}
+
+func TestDecodeAESNotBlockAligned(t *testing.T) {
+ setupKYCViper(t)
+
+ // Encode a short plaintext so the resulting ciphertext is valid base64
+ // but deliberately corrupt it to produce non-block-aligned data.
+ encoded, err := EncodeAES(&PassportInfo{ID: "x"})
+ require.NoError(t, err)
+
+ // Trim one character from the end to break block alignment.
+ corrupted := (*encoded)[:len(*encoded)-1]
+ _, err = DecodeAES(corrupted)
+ assert.Error(t, err)
+}
+
+func TestDecodeAESWrongKey(t *testing.T) {
+ setupKYCViper(t)
+
+ // Encode with the correct key.
+ encoded, err := EncodeAES(&CNRidInfo{LegalName: "王五", ResidentId: "440101200001011234"})
+ require.NoError(t, err)
+
+ // Decode with a different key — must fail.
+ viper.Set("secrets.kyc_info_key", "wrongkey12345678")
+ _, err = DecodeAES(*encoded)
+ assert.Error(t, err)
+}
diff --git a/internal/kyc/passport.go b/internal/kyc/passport.go
new file mode 100644
index 0000000..ef5941f
--- /dev/null
+++ b/internal/kyc/passport.go
@@ -0,0 +1,91 @@
+package kyc
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/spf13/viper"
+)
+
+const (
+ StateCreated = "CREATED"
+ StateInitiated = "INITIATED"
+ StateFailed = "FAILED"
+ StateAborted = "ABORTED"
+ StateCompleted = "COMPLETED"
+ StateRejected = "REJECTED"
+ StateApproved = "APPROVED"
+)
+
+func doPassportRequest(ctx context.Context, method, path string, body any, target any) error {
+ baseURL := viper.GetString("kyc.passport_reader_endpoint")
+ publicKey := viper.GetString("kyc.passport_reader_public_key")
+ secret := viper.GetString("kyc.passport_reader_secret")
+
+ var bodyReader io.Reader
+ if body != nil {
+ jsonData, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("marshal request failed: %w", err)
+ }
+ bodyReader = bytes.NewBuffer(jsonData)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bodyReader)
+ if err != nil {
+ return fmt.Errorf("create request failed: %w", err)
+ }
+
+ req.SetBasicAuth(publicKey, secret)
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ client := &http.Client{Timeout: 15 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("http request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ respBody, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("api error: status %d, body %s", resp.StatusCode, string(respBody))
+ }
+
+ if target != nil {
+ if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
+ return fmt.Errorf("decode response failed: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func CreateSession(ctx context.Context) (*PassportReaderSessionResponse, error) {
+ var resp PassportReaderSessionResponse
+ err := doPassportRequest(ctx, "POST", "/session.create", nil, &resp)
+ return &resp, err
+}
+
+func GetSessionState(ctx context.Context, sessionID int) (string, error) {
+ payload := PassportReaderGetSessionRequest{ID: sessionID}
+ var resp PassportReaderStateResponse
+ err := doPassportRequest(ctx, "POST", "/session.state", payload, &resp)
+ if err != nil {
+ return "", err
+ }
+ return resp.State, nil
+}
+
+func GetSessionDetails(ctx context.Context, sessionID int) (*PassportReaderSessionDetailResponse, error) {
+ payload := PassportReaderGetSessionRequest{ID: sessionID}
+ var resp PassportReaderSessionDetailResponse
+ err := doPassportRequest(ctx, "POST", "/session.get", payload, &resp)
+ return &resp, err
+}
diff --git a/internal/kyc/passport_test.go b/internal/kyc/passport_test.go
new file mode 100644
index 0000000..f15877b
--- /dev/null
+++ b/internal/kyc/passport_test.go
@@ -0,0 +1,191 @@
+package kyc
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// startMockPassportServer spins up an httptest.Server backed by mux, points
+// viper's kyc.passport_reader_endpoint at it, and registers cleanup on t.
+func startMockPassportServer(t *testing.T, mux *http.ServeMux) *httptest.Server {
+ t.Helper()
+ srv := httptest.NewServer(mux)
+ viper.Reset()
+ viper.Set("kyc.passport_reader_endpoint", srv.URL)
+ viper.Set("kyc.passport_reader_public_key", "pub")
+ viper.Set("kyc.passport_reader_secret", "sec")
+ t.Cleanup(func() {
+ srv.Close()
+ viper.Reset()
+ })
+ return srv
+}
+
+func writeJSON(w http.ResponseWriter, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(v)
+}
+
+// ---- CreateSession ----
+
+func TestCreateSessionSuccess(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.create", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodPost, r.Method)
+ writeJSON(w, PassportReaderSessionResponse{ID: 42, Token: "tok"})
+ })
+ startMockPassportServer(t, mux)
+
+ resp, err := CreateSession(context.Background())
+ require.NoError(t, err)
+ assert.Equal(t, 42, resp.ID)
+ assert.Equal(t, "tok", resp.Token)
+}
+
+func TestCreateSessionAPIError(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.create", func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ })
+ startMockPassportServer(t, mux)
+
+ _, err := CreateSession(context.Background())
+ assert.Error(t, err)
+}
+
+// ---- GetSessionState ----
+
+func TestGetSessionStateSuccess(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodPost, r.Method)
+ assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+ var req PassportReaderGetSessionRequest
+ require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
+ assert.Equal(t, 42, req.ID)
+ writeJSON(w, PassportReaderStateResponse{State: StateApproved})
+ })
+ startMockPassportServer(t, mux)
+
+ state, err := GetSessionState(context.Background(), 42)
+ require.NoError(t, err)
+ assert.Equal(t, StateApproved, state)
+}
+
+func TestGetSessionStateEachKnownState(t *testing.T) {
+ knownStates := []string{
+ StateCreated, StateInitiated, StateCompleted,
+ StateApproved, StateFailed, StateAborted, StateRejected,
+ }
+
+ for _, s := range knownStates {
+ s := s
+ t.Run(s, func(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ writeJSON(w, PassportReaderStateResponse{State: s})
+ })
+ startMockPassportServer(t, mux)
+
+ got, err := GetSessionState(context.Background(), 1)
+ require.NoError(t, err)
+ assert.Equal(t, s, got)
+ })
+ }
+}
+
+func TestGetSessionStateAPIError(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "bad", http.StatusBadRequest)
+ })
+ startMockPassportServer(t, mux)
+
+ _, err := GetSessionState(context.Background(), 1)
+ assert.Error(t, err)
+}
+
+// ---- GetSessionDetails ----
+
+func TestGetSessionDetailsSuccess(t *testing.T) {
+ want := &PassportReaderSessionDetailResponse{
+ State: StateApproved,
+ GivenNames: "John",
+ Surname: "Doe",
+ Nationality: "USA",
+ DateOfBirth: "1990-01-01",
+ DocumentType: "PASSPORT",
+ DocumentNumber: "X12345678",
+ ExpiryDate: "2030-01-01",
+ }
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.get", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodPost, r.Method)
+ assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+ var req PassportReaderGetSessionRequest
+ require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
+ assert.Equal(t, 7, req.ID)
+ writeJSON(w, want)
+ })
+ startMockPassportServer(t, mux)
+
+ got, err := GetSessionDetails(context.Background(), 7)
+ require.NoError(t, err)
+ assert.Equal(t, want, got)
+}
+
+func TestGetSessionDetailsAPIError(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.get", func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "not found", http.StatusNotFound)
+ })
+ startMockPassportServer(t, mux)
+
+ _, err := GetSessionDetails(context.Background(), 1)
+ assert.Error(t, err)
+}
+
+// ---- HTTP mechanics ----
+
+func TestPassportRequestSendsBasicAuth(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.create", func(w http.ResponseWriter, r *http.Request) {
+ user, pass, ok := r.BasicAuth()
+ assert.True(t, ok, "Authorization header must be present")
+ assert.Equal(t, "pub", user)
+ assert.Equal(t, "sec", pass)
+ writeJSON(w, PassportReaderSessionResponse{})
+ })
+ startMockPassportServer(t, mux)
+
+ _, _ = CreateSession(context.Background())
+}
+
+func TestPassportRequestSetsContentTypeForBody(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+ writeJSON(w, PassportReaderStateResponse{State: StateCreated})
+ })
+ startMockPassportServer(t, mux)
+
+ _, _ = GetSessionState(context.Background(), 1)
+}
+
+func TestPassportRequestNoContentTypeWithoutBody(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.create", func(w http.ResponseWriter, r *http.Request) {
+ assert.Empty(t, r.Header.Get("Content-Type"), "no Content-Type when body is nil")
+ writeJSON(w, PassportReaderSessionResponse{})
+ })
+ startMockPassportServer(t, mux)
+
+ _, _ = CreateSession(context.Background())
+}
diff --git a/internal/kyc/types.go b/internal/kyc/types.go
index 9bdbece..bcbd63f 100644
--- a/internal/kyc/types.go
+++ b/internal/kyc/types.go
@@ -1,17 +1,50 @@
package kyc
-type KycInfo struct {
- Type string `json:"type"` // cnrid/passport
- LegalName string `json:"legal_name"`
- ResidentId string `json:"rsident_id"`
- PassportInfo PassportInfo `json:"passport_info"`
+type CNRidInfo struct {
+ LegalName string `json:"legal_name"`
+ ResidentId string `json:"resident_id"`
}
type PassportInfo struct {
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- DateOfExpire string `json:"date_of_expire"`
+ ID string `json:"id"`
+}
+
+type PassportResp struct {
+ GivenNames string `json:"given_names"`
+ Surname string `json:"surname"`
Nationality string `json:"nationality"`
+ DateOfBirth string `json:"date_of_birth"`
DocumentType string `json:"document_type"`
DocumentNumber string `json:"document_number"`
+ ExpiryDate string `json:"expiry_date"`
+}
+
+type AliCloudAuth struct {
+ ParamType string `json:"param_type"`
+ IdentifyNum string `json:"identify_num"`
+ UserName string `json:"user_name"`
+}
+
+type PassportReaderSessionResponse struct {
+ ID int `json:"id"`
+ Token string `json:"token"`
+}
+
+type PassportReaderGetSessionRequest struct {
+ ID int `json:"id"`
+}
+
+type PassportReaderStateResponse struct {
+ State string `json:"state"`
+}
+
+type PassportReaderSessionDetailResponse struct {
+ State string `json:"state"`
+ GivenNames string `json:"given_names"`
+ Surname string `json:"surname"`
+ Nationality string `json:"nationality"`
+ DateOfBirth string `json:"date_of_birth"`
+ DocumentType string `json:"document_type"`
+ DocumentNumber string `json:"document_number"`
+ ExpiryDate string `json:"expiry_date"`
}
diff --git a/internal/sc_realid/types.go b/internal/sc_realid/types.go
deleted file mode 100644
index 2f98ca4..0000000
--- a/internal/sc_realid/types.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package screalid
-
-type KycPassportResponse struct {
- Status string `json:"status"`
- FinalResult struct {
- FirstName string `json:"firstName"`
- LastName string `json:"lastName"`
- DateOfExpire string `json:"dateOfExpire"`
- Nationality string `json:"nationality"`
- DocumentType string `json:"documentType"`
- DocumentNumber string `json:"documentNumber"`
- } `json:"finalResult"`
-}
diff --git a/internal/turnstile/turnstile_test.go b/internal/turnstile/turnstile_test.go
new file mode 100644
index 0000000..c67c1c8
--- /dev/null
+++ b/internal/turnstile/turnstile_test.go
@@ -0,0 +1,89 @@
+package turnstile
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// redirectTransport rewrites all outgoing requests to a test server,
+// then forwards them using the *original* transport to avoid recursion.
+type redirectTransport struct {
+ target string
+ orig http.RoundTripper
+}
+
+func (r *redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ u, err := url.Parse(r.target)
+ if err != nil {
+ return nil, err
+ }
+ req.URL.Scheme = u.Scheme
+ req.URL.Host = u.Host
+ return r.orig.RoundTrip(req)
+}
+
+// patchTransport replaces http.DefaultTransport with one that redirects
+// all requests to srv. It restores the original on test cleanup.
+func patchTransport(t *testing.T, srv *httptest.Server) {
+ t.Helper()
+ orig := http.DefaultTransport
+ http.DefaultTransport = &redirectTransport{target: srv.URL, orig: orig}
+ t.Cleanup(func() { http.DefaultTransport = orig })
+}
+
+func cfMockServer(success bool) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(struct {
+ Success bool `json:"success"`
+ }{success})
+ }))
+}
+
+func TestVerifyTurnstileSuccess(t *testing.T) {
+ srv := cfMockServer(true)
+ defer srv.Close()
+ patchTransport(t, srv)
+
+ viper.Set("secrets.turnstile_secret", "test-secret")
+ t.Cleanup(viper.Reset)
+
+ ok, err := VerifyTurnstile("valid-token", "127.0.0.1")
+ require.NoError(t, err)
+ assert.True(t, ok)
+}
+
+func TestVerifyTurnstileFailure(t *testing.T) {
+ srv := cfMockServer(false)
+ defer srv.Close()
+ patchTransport(t, srv)
+
+ viper.Set("secrets.turnstile_secret", "test-secret")
+ t.Cleanup(viper.Reset)
+
+ ok, err := VerifyTurnstile("invalid-token", "127.0.0.1")
+ require.NoError(t, err)
+ assert.False(t, ok)
+}
+
+func TestVerifyTurnstileServerError(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte("not json"))
+ }))
+ defer srv.Close()
+ patchTransport(t, srv)
+
+ viper.Set("secrets.turnstile_secret", "test-secret")
+ t.Cleanup(viper.Reset)
+
+ _, err := VerifyTurnstile("token", "127.0.0.1")
+ require.Error(t, err)
+}
diff --git a/justfile b/justfile
index 79e84cf..639d10e 100644
--- a/justfile
+++ b/justfile
@@ -1,51 +1,35 @@
project_name := "nixcn-cms"
go_cmd := `realpath $(which go)`
-pnpm_cmd := `realpath $(which pnpm)`
project_dir := justfile_directory()
output_dir := join(project_dir, ".outputs")
-client_dir := join(project_dir, "client")
-client_output_dir := join(output_dir, "client")
-client_cms_dir := join(client_dir, "cms")
server_exec_path := join(output_dir, project_name)
server_entry := "main.go"
-install: install-cms install-back
+install:
+ go install github.com/swaggo/swag/cmd/swag@latest
+ cd {{ project_dir }} && go mod tidy
-generate: gen-back
-
-install-cms:
- cd {{ client_cms_dir }} && {{ pnpm_cmd }} install
+generate:
+ cd {{ project_dir }} && go generate .
clean:
mkdir -p .outputs
find .outputs -mindepth 1 ! -path .outputs/config.yaml -exec rm -rf {} +
-build-client-cms:
- cd {{ client_cms_dir }} && {{ pnpm_cmd }} run build --outDir {{ join(client_output_dir, "cms") }}
-
-build-back:
+build:
{{ go_cmd }} build -o {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }} {{ server_entry }}
-install-back:
- cd {{ project_dir }} && go mod tidy
-
-run-back:
+run:
cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }}
-test-back:
- cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. ./...
+test:
+ cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. -p 4 ./...
-gen-back:
- cd {{ project_dir }} && go generate .
-
-watch-back:
+watch:
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
-dev-client-cms: install-cms
- devenv up client-cms --verbose
-
-dev-back: clean install-back gen-back
- devenv up backend postgres redis meilisearch lgtm --verbose
+back: clean install generate
+ devenv up postgres redis lgtm --verbose
diff --git a/logger/logger_test.go b/logger/logger_test.go
new file mode 100644
index 0000000..76b366f
--- /dev/null
+++ b/logger/logger_test.go
@@ -0,0 +1,147 @@
+package logger
+
+import (
+ "bytes"
+ "context"
+ "log/slog"
+ "os"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ gormlogger "gorm.io/gorm/logger"
+)
+
+// ---------- multiHandler ----------
+
+type countingHandler struct {
+ enabled bool
+ handled int
+ attrs []slog.Attr
+ group string
+}
+
+func (h *countingHandler) Enabled(_ context.Context, _ slog.Level) bool { return h.enabled }
+func (h *countingHandler) Handle(_ context.Context, _ slog.Record) error {
+ h.handled++
+ return nil
+}
+func (h *countingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return &countingHandler{enabled: h.enabled, attrs: attrs}
+}
+func (h *countingHandler) WithGroup(name string) slog.Handler {
+ return &countingHandler{enabled: h.enabled, group: name}
+}
+
+func TestMultiHandlerEnabledAny(t *testing.T) {
+ a := &countingHandler{enabled: false}
+ b := &countingHandler{enabled: true}
+ mh := &multiHandler{handlers: []slog.Handler{a, b}}
+ assert.True(t, mh.Enabled(context.Background(), slog.LevelInfo))
+}
+
+func TestMultiHandlerEnabledNone(t *testing.T) {
+ a := &countingHandler{enabled: false}
+ b := &countingHandler{enabled: false}
+ mh := &multiHandler{handlers: []slog.Handler{a, b}}
+ assert.False(t, mh.Enabled(context.Background(), slog.LevelInfo))
+}
+
+func TestMultiHandlerHandle(t *testing.T) {
+ a := &countingHandler{enabled: true}
+ b := &countingHandler{enabled: true}
+ mh := &multiHandler{handlers: []slog.Handler{a, b}}
+
+ var r slog.Record
+ r = slog.NewRecord(r.Time, slog.LevelInfo, "hello", 0)
+ require.NoError(t, mh.Handle(context.Background(), r))
+ assert.Equal(t, 1, a.handled)
+ assert.Equal(t, 1, b.handled)
+}
+
+func TestMultiHandlerWithAttrs(t *testing.T) {
+ a := &countingHandler{enabled: true}
+ mh := &multiHandler{handlers: []slog.Handler{a}}
+ attrs := []slog.Attr{slog.String("k", "v")}
+ result := mh.WithAttrs(attrs)
+ require.NotNil(t, result)
+ inner, ok := result.(*multiHandler)
+ require.True(t, ok)
+ assert.Len(t, inner.handlers, 1)
+}
+
+func TestMultiHandlerWithGroup(t *testing.T) {
+ a := &countingHandler{enabled: true}
+ mh := &multiHandler{handlers: []slog.Handler{a}}
+ result := mh.WithGroup("mygroup")
+ require.NotNil(t, result)
+ inner, ok := result.(*multiHandler)
+ require.True(t, ok)
+ assert.Len(t, inner.handlers, 1)
+}
+
+// ---------- SlogWriter ----------
+
+func TestSlogWriterPrintf(t *testing.T) {
+ // Redirect slog to a buffer so the Printf output can be verified.
+ var buf bytes.Buffer
+ h := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
+ old := slog.Default()
+ slog.SetDefault(slog.New(h))
+ defer slog.SetDefault(old)
+
+ w := &SlogWriter{}
+ assert.NotPanics(t, func() {
+ w.Printf("hello %s %d", "world", 42)
+ })
+ assert.Contains(t, buf.String(), "hello world 42")
+}
+
+// ---------- GormLogger ----------
+
+func TestGormLoggerNotNil(t *testing.T) {
+ l := GormLogger()
+ require.NotNil(t, l)
+ _, ok := l.(gormlogger.Interface)
+ assert.True(t, ok)
+}
+
+// ---------- Init ----------
+
+func TestLoggerInitLevelInfo(t *testing.T) {
+ viper.Set("server.log_level", "info")
+ viper.Set("server.service_name", "test-svc")
+ defer viper.Reset()
+ assert.NotPanics(t, func() { Init() })
+}
+
+func TestLoggerInitLevelDebug(t *testing.T) {
+ t.Cleanup(func() { os.Remove("app.log") })
+ viper.Set("server.log_level", "debug")
+ viper.Set("server.service_name", "test-svc")
+ defer viper.Reset()
+ assert.NotPanics(t, func() { Init() })
+}
+
+func TestLoggerInitLevelWarn(t *testing.T) {
+ viper.Set("server.log_level", "warn")
+ viper.Set("server.service_name", "test-svc")
+ defer viper.Reset()
+ assert.NotPanics(t, func() { Init() })
+}
+
+func TestLoggerInitLevelError(t *testing.T) {
+ viper.Set("server.log_level", "error")
+ viper.Set("server.service_name", "test-svc")
+ defer viper.Reset()
+ assert.NotPanics(t, func() { Init() })
+}
+
+func TestLoggerInitUnknownLevel(t *testing.T) {
+ viper.Set("server.log_level", "verbose")
+ viper.Set("server.service_name", "test-svc")
+ defer viper.Reset()
+ // Unknown level falls back to info — must not panic.
+ assert.NotPanics(t, func() { Init() })
+}
diff --git a/middleware/api_version.go b/middleware/api_version.go
deleted file mode 100644
index a13b2ac..0000000
--- a/middleware/api_version.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package middleware
-
-import (
- "nixcn-cms/internal/exception"
- "nixcn-cms/utils"
-
- "github.com/gin-gonic/gin"
-)
-
-func ApiVersionCheck() gin.HandlerFunc {
- return func(c *gin.Context) {
- apiVersion := c.GetHeader("X-Api-Version")
- if apiVersion == "" {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.MiddlewareServiceApiVersion).
- SetEndpoint(exception.EndpointMiddlewareService).
- SetType(exception.TypeSpecific).
- SetOriginal(exception.ApiVersionNotFound).
- Throw(c).
- String()
- utils.HttpAbort(c, 400, errorCode)
- return
- }
- c.Next()
- }
-}
diff --git a/middleware/gin_logger.go b/middleware/gin_logger.go
index 18dc0a2..0792ee2 100644
--- a/middleware/gin_logger.go
+++ b/middleware/gin_logger.go
@@ -6,13 +6,31 @@ import (
"fmt"
"io"
"log/slog"
+ "nixcn-cms/tracer"
+ "runtime"
"time"
"github.com/gin-gonic/gin"
)
func GinLogger() gin.HandlerFunc {
+ pc, filePath, line, _ := runtime.Caller(0)
+ entryInfo := tracer.AdditionSpanLayerInfo{
+ LayerName: "gin-middleware",
+ FuncName: runtime.FuncForPC(pc).Name(),
+ FilePath: filePath,
+ Line: line,
+ }
+
return func(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "middleware_gin_logger",
+ "slog",
+ entryInfo,
+ )
+ defer span.End()
+
var body []byte
if c.Request.Body != nil {
body, _ = io.ReadAll(c.Request.Body)
@@ -23,10 +41,9 @@ func GinLogger() gin.HandlerFunc {
startTime := time.Now()
+ c.Request = c.Request.WithContext(ctx)
c.Next()
- ctx := c.Request.Context()
-
var errorMessage string
if len(c.Errors) > 0 {
errorMessage = c.Errors.String()
@@ -44,6 +61,11 @@ func GinLogger() gin.HandlerFunc {
"errors", errorMessage,
}
+ if len(c.Errors) > 0 {
+ fields = append(fields, "gin_errors", c.Errors.String())
+ span.RecordError(fmt.Errorf("%s", c.Errors.String()))
+ }
+
status := c.Writer.Status()
if len(c.Errors) > 0 || status >= 500 {
slog.ErrorContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
diff --git a/middleware/jwt.go b/middleware/jwt.go
index 25cc765..1cc6867 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -3,33 +3,60 @@ package middleware
import (
"nixcn-cms/internal/authtoken"
"nixcn-cms/internal/exception"
+ "nixcn-cms/tracer"
"nixcn-cms/utils"
+ "runtime"
"github.com/gin-gonic/gin"
+ "go.opentelemetry.io/otel/attribute"
)
+// @securityDefinitions.apikey Bearer
+// @in header
+// @name Authorization
func JWTAuth() gin.HandlerFunc {
+ pc, filePath, line, _ := runtime.Caller(0)
+ entryInfo := tracer.AdditionSpanLayerInfo{
+ LayerName: "gin-middleware",
+ FuncName: runtime.FuncForPC(pc).Name(),
+ FilePath: filePath,
+ Line: line,
+ }
return func(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "middleware_jwt",
+ "verify",
+ entryInfo,
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.MiddlewareServiceJwt)
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointMiddleware)
+
auth := c.GetHeader("Authorization")
-
authtoken := new(authtoken.Token)
- uid, err := authtoken.HeaderVerify(c, auth)
- if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.MiddlewareServiceJwt).
- SetEndpoint(exception.EndpointMiddlewareService).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorUnauthorized).
- SetError(err).
- Throw(c).
- String()
+ uid, err := authtoken.HeaderVerify(ctx, auth)
+ if uid != "" {
+ span.SetAttributes(attribute.String("user.id", uid))
+ }
+
+ if err != nil {
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUnauthorized),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ c.Request = c.Request.WithContext(ctx)
utils.HttpAbort(c, 401, errorCode)
return
}
+ c.Request = c.Request.WithContext(ctx)
c.Set("user_id", uid)
c.Next()
}
diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go
new file mode 100644
index 0000000..377e72f
--- /dev/null
+++ b/middleware/middleware_test.go
@@ -0,0 +1,284 @@
+package middleware
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+// issueToken generates a real JWT access token for the given user UUID.
+func issueToken(t *testing.T, userId uuid.UUID) string {
+ t.Helper()
+ tok := &authtoken.Token{Application: viper.GetString("server.application")}
+ access, _, err := tok.IssueTokens(context.Background(), testutil.TestClientID, userId)
+ require.NoError(t, err)
+ return access
+}
+
+// ---- GinLogger ----
+
+func TestGinLogger200(t *testing.T) {
+ testutil.Setup(t)
+ r := gin.New()
+ r.Use(GinLogger())
+ r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") })
+
+ req := httptest.NewRequest(http.MethodGet, "/ping", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "pong", w.Body.String())
+}
+
+func TestGinLogger400(t *testing.T) {
+ testutil.Setup(t)
+ r := gin.New()
+ r.Use(GinLogger())
+ r.GET("/bad", func(c *gin.Context) { c.Status(400) })
+
+ req := httptest.NewRequest(http.MethodGet, "/bad", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 400, w.Code)
+}
+
+func TestGinLogger500(t *testing.T) {
+ testutil.Setup(t)
+ r := gin.New()
+ r.Use(GinLogger())
+ r.GET("/err", func(c *gin.Context) { c.Status(500) })
+
+ req := httptest.NewRequest(http.MethodGet, "/err", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 500, w.Code)
+}
+
+func TestGinLoggerBodyPreserved(t *testing.T) {
+ testutil.Setup(t)
+ var captured string
+ r := gin.New()
+ r.Use(GinLogger())
+ r.POST("/echo", func(c *gin.Context) {
+ var m map[string]string
+ require.NoError(t, c.ShouldBindJSON(&m))
+ captured = m["msg"]
+ c.String(200, "ok")
+ })
+
+ body, _ := json.Marshal(map[string]string{"msg": "hello"})
+ req := httptest.NewRequest(http.MethodPost, "/echo", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "hello", captured)
+}
+
+// ---- JWTAuth ----
+
+func TestJWTAuthNoHeader(t *testing.T) {
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+ r := gin.New()
+ r.Use(JWTAuth())
+ r.GET("/protected", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/protected", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 401, w.Code)
+}
+
+func TestJWTAuthBadFormat(t *testing.T) {
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+ r := gin.New()
+ r.Use(JWTAuth())
+ r.GET("/protected", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/protected", nil)
+ req.Header.Set("Authorization", "not-a-valid-token")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 401, w.Code)
+}
+
+func TestJWTAuthValidToken(t *testing.T) {
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 0)
+ token := issueToken(t, user.UUID)
+
+ r := gin.New()
+ r.Use(JWTAuth())
+ r.GET("/protected", func(c *gin.Context) {
+ uid, _ := c.Get("user_id")
+ c.String(200, uid.(string))
+ })
+
+ req := httptest.NewRequest(http.MethodGet, "/protected", nil)
+ req.Header.Set("Authorization", "Bearer "+token)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, user.UUID.String(), w.Body.String())
+}
+
+// ---- Permission ----
+
+func TestPermissionPresetSufficient(t *testing.T) {
+ testutil.Setup(t)
+ r := gin.New()
+ r.Use(func(c *gin.Context) {
+ c.Set("user_id", uuid.New().String())
+ c.Set("permission_level", uint(10))
+ c.Next()
+ })
+ r.Use(Permission(5))
+ r.GET("/admin", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/admin", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 200, w.Code)
+}
+
+func TestPermissionPresetInsufficient(t *testing.T) {
+ testutil.Setup(t)
+ r := gin.New()
+ r.Use(func(c *gin.Context) {
+ c.Set("user_id", uuid.New().String())
+ c.Set("permission_level", uint(1))
+ c.Next()
+ })
+ r.Use(Permission(99))
+ r.GET("/admin", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/admin", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 403, w.Code)
+}
+
+func TestPermissionNoUserId(t *testing.T) {
+ testutil.Setup(t)
+ r := gin.New()
+ r.Use(Permission(0))
+ r.GET("/any", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/any", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 401, w.Code)
+}
+
+func TestPermissionFromDBSufficient(t *testing.T) {
+ testutil.Setup(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+
+ r := gin.New()
+ r.Use(func(c *gin.Context) {
+ // Permission middleware looks up by the user_id column.
+ c.Set("user_id", user.UserId.String())
+ c.Next()
+ })
+ r.Use(Permission(5))
+ r.GET("/admin", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/admin", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 200, w.Code)
+}
+
+func TestPermissionFromDBInsufficient(t *testing.T) {
+ testutil.Setup(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 1)
+
+ r := gin.New()
+ r.Use(func(c *gin.Context) {
+ c.Set("user_id", user.UserId.String())
+ c.Next()
+ })
+ r.Use(Permission(99))
+ r.GET("/admin", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/admin", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 403, w.Code)
+}
+
+func TestPermissionUserNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ r := gin.New()
+ r.Use(func(c *gin.Context) {
+ c.Set("user_id", uuid.New().String())
+ c.Next()
+ })
+ r.Use(Permission(0))
+ r.GET("/any", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/any", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 404, w.Code)
+}
+
+func TestPermissionCachesLevelFromDB(t *testing.T) {
+ testutil.Setup(t)
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+
+ calls := 0
+ r := gin.New()
+ r.Use(func(c *gin.Context) {
+ c.Set("user_id", user.UserId.String())
+ calls++
+ c.Next()
+ })
+ r.Use(Permission(5))
+ r.GET("/any", func(c *gin.Context) {
+ lvl, ok := c.Get("permission_level")
+ assert.True(t, ok)
+ assert.Equal(t, uint(10), lvl.(uint))
+ c.String(200, "ok")
+ })
+
+ req := httptest.NewRequest(http.MethodGet, "/any", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 200, w.Code)
+}
+
+func TestJWTAuthBadToken(t *testing.T) {
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+
+ r := gin.New()
+ r.Use(JWTAuth())
+ r.GET("/protected", func(c *gin.Context) { c.String(200, "ok") })
+
+ req := httptest.NewRequest(http.MethodGet, "/protected", nil)
+ req.Header.Set("Authorization", "Bearer completely.invalid.token")
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+ assert.Equal(t, 401, w.Code)
+}
diff --git a/middleware/permission.go b/middleware/permission.go
index d3879cb..fc9bf92 100644
--- a/middleware/permission.go
+++ b/middleware/permission.go
@@ -1,61 +1,85 @@
package middleware
import (
+ "errors"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
+ "nixcn-cms/tracer"
"nixcn-cms/utils"
+ "runtime"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
+ "go.opentelemetry.io/otel/attribute"
)
func Permission(requiredLevel uint) gin.HandlerFunc {
+ pc, filePath, line, _ := runtime.Caller(0)
+ entryInfo := tracer.AdditionSpanLayerInfo{
+ LayerName: "gin-middleware",
+ FuncName: runtime.FuncForPC(pc).Name(),
+ FilePath: filePath,
+ Line: line,
+ }
+
return func(c *gin.Context) {
+ ctx, span := tracer.StartSpan(
+ c.Request.Context(),
+ "middleware_permission",
+ "check",
+ entryInfo,
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.MiddlewareServicePermission)
+ ctx = exception.ContextWithEndpoint(ctx, exception.EndpointMiddleware)
+
var permissionLevel uint
permissionLevelPrev, ok := c.Get("permission_level")
if !ok {
userIdOrig, ok := c.Get("user_id")
if !ok || userIdOrig.(string) == "" {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.MiddlewareServicePermission).
- SetEndpoint(exception.EndpointMiddlewareService).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorMissingUserId).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorMissingUserId),
+ exception.WithError(errors.New("Missing UserId")),
+ ).Throw(ctx).String()
+ c.Request = c.Request.WithContext(ctx)
utils.HttpAbort(c, 401, errorCode)
return
}
+ span.SetAttributes(attribute.String("user.id", userIdOrig.(string)))
+
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.MiddlewareServicePermission).
- SetEndpoint(exception.EndpointMiddlewareService).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorUuidParseFailed).
- SetError(err).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+
+ c.Request = c.Request.WithContext(ctx)
utils.HttpAbort(c, 500, errorCode)
return
}
userData, err := new(data.User).GetByUserId(c, &userId)
- if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.MiddlewareServicePermission).
- SetEndpoint(exception.EndpointMiddlewareService).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorUserNotFound).
- SetError(err).
- Throw(c).
- String()
+ if err != nil || userData == nil {
+ if err == nil {
+ err = errors.New("user not found")
+ }
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUserNotFound),
+ exception.WithError(err),
+ ).Throw(ctx).String()
+ c.Request = c.Request.WithContext(ctx)
utils.HttpAbort(c, 404, errorCode)
return
}
@@ -67,19 +91,20 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
}
if permissionLevel < requiredLevel {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.MiddlewareServicePermission).
- SetEndpoint(exception.EndpointMiddlewareService).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonErrorPermissionDenied).
- Throw(c).
- String()
+ errorCode := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("Permission Denied")),
+ ).Throw(ctx).String()
+
+ c.Request = c.Request.WithContext(ctx)
utils.HttpAbort(c, 403, errorCode)
return
}
+ c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
diff --git a/server/server.go b/server/server.go
index 9bbff23..1bdc513 100644
--- a/server/server.go
+++ b/server/server.go
@@ -11,9 +11,29 @@ 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"
+
+ _ "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 /app/api/v1
+// @schemes http https
+//
+// @securityDefinitions.apikey Bearer
+// @in header
+// @name Authorization
func Start(ctx context.Context) {
if !viper.GetBool("server.debug_mode") {
gin.SetMode(gin.ReleaseMode)
@@ -23,9 +43,14 @@ func Start(ctx context.Context) {
r := gin.New()
r.Use(otelgin.Middleware(viper.GetString("server.service_name")))
r.Use(middleware.GinLogger())
+
+ if viper.GetBool("server.debug_mode") {
+ r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+ }
+
r.Use(gin.Recovery())
- api.Handler(r.Group("/api/v1"))
+ api.Handler(r.Group("/app/api/v1"))
// Start http server
server := &http.Server{
diff --git a/server/server_test.go b/server/server_test.go
new file mode 100644
index 0000000..edde417
--- /dev/null
+++ b/server/server_test.go
@@ -0,0 +1,119 @@
+package server
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
+ "nixcn-cms/api"
+ "nixcn-cms/middleware"
+ "nixcn-cms/testutil"
+)
+
+func init() { gin.SetMode(gin.TestMode) }
+
+// buildRouter mirrors the engine setup inside Start() without blocking on
+// ListenAndServe, making it possible to use httptest.
+func buildRouter() *gin.Engine {
+ r := gin.New()
+ r.Use(otelgin.Middleware(viper.GetString("server.service_name")))
+ r.Use(middleware.GinLogger())
+ r.Use(gin.Recovery())
+ api.Handler(r.Group("/app/api/v1"))
+ return r
+}
+
+func TestServerRouterResponds(t *testing.T) {
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+
+ r := buildRouter()
+ srv := httptest.NewServer(r)
+ defer srv.Close()
+
+ // POST /auth/magic exists — any response other than 404 confirms routing.
+ resp, err := http.Post(srv.URL+"/app/api/v1/auth/magic", "application/json", nil)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.NotEqual(t, http.StatusNotFound, resp.StatusCode)
+}
+
+func TestServerRouterNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ r := buildRouter()
+ req := httptest.NewRequest(http.MethodGet, "/app/api/v1/nonexistent", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
+
+func TestServerRouterRecovery(t *testing.T) {
+ testutil.Setup(t)
+
+ r := gin.New()
+ r.Use(gin.Recovery())
+ r.GET("/panic", func(c *gin.Context) { panic("test panic") })
+
+ req := httptest.NewRequest(http.MethodGet, "/panic", nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ // Recovery middleware must catch the panic and return 500.
+ assert.Equal(t, http.StatusInternalServerError, w.Code)
+}
+
+func TestServerModeRelease(t *testing.T) {
+ viper.Set("server.debug_mode", false)
+ viper.Set("server.service_name", "test-svc")
+ defer viper.Reset()
+
+ // Calling Start() would block, so we replicate only the mode-setting logic.
+ if !viper.GetBool("server.debug_mode") {
+ gin.SetMode(gin.ReleaseMode)
+ }
+ assert.Equal(t, gin.ReleaseMode, gin.Mode())
+ gin.SetMode(gin.TestMode) // restore
+}
+
+func TestServerHealthEndpoints(t *testing.T) {
+ testutil.Setup(t)
+ testutil.SeedClient(t)
+
+ r := buildRouter()
+
+ endpoints := []struct {
+ method string
+ path string
+ }{
+ {http.MethodPost, "/app/api/v1/auth/magic"},
+ {http.MethodPost, "/app/api/v1/auth/exchange"},
+ {http.MethodGet, "/app/api/v1/event/list"},
+ {http.MethodGet, "/app/api/v1/user/info"},
+ }
+
+ for _, ep := range endpoints {
+ t.Run(ep.method+" "+ep.path, func(t *testing.T) {
+ req := httptest.NewRequest(ep.method, ep.path, nil)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, req)
+
+ // Route must be registered — anything other than 404 is fine.
+ assert.NotEqual(t, http.StatusNotFound, w.Code,
+ "route %s %s should be registered", ep.method, ep.path)
+
+ // Response must be valid JSON.
+ var body map[string]any
+ err := json.Unmarshal(w.Body.Bytes(), &body)
+ assert.NoError(t, err, "response must be valid JSON for %s %s", ep.method, ep.path)
+ })
+ }
+}
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/magic.go b/service/auth/magic.go
deleted file mode 100644
index a5a7568..0000000
--- a/service/auth/magic.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package auth
-
-import (
- "net/url"
- "nixcn-cms/internal/exception"
- "nixcn-cms/pkgs/authcode"
- "nixcn-cms/pkgs/email"
- "nixcn-cms/pkgs/turnstile"
- "nixcn-cms/utils"
-
- "github.com/gin-gonic/gin"
- "github.com/spf13/viper"
-)
-
-type MagicRequest struct {
- ClientId string `json:"client_id"`
- RedirectUri string `json:"redirect_uri"`
- State string `json:"state"`
- Email string `json:"email"`
- TurnstileToken string `json:"turnstile_token"`
-}
-
-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
- }
-
- // Cloudflare turnstile
- ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP())
- if err != nil || !ok {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusUser).
- SetService(exception.ServiceAuth).
- SetEndpoint(exception.EndpointAuthServiceMagic).
- SetType(exception.TypeSpecific).
- SetOriginal(exception.AuthMagicTurnstileFailed).
- SetError(err).
- Build(c)
- utils.HttpResponse(c, 403, errorCode)
- return
- }
-
- code, err := authcode.NewAuthCode(c, req.ClientId, req.Email)
- if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.ServiceAuth).
- SetEndpoint(exception.EndpointAuthServiceMagic).
- SetType(exception.TypeSpecific).
- SetOriginal(exception.AuthMagicCodeGenFailed).
- SetError(err).
- Build(c)
- utils.HttpResponse(c, 500, errorCode)
- return
- }
-
- externalUrl := viper.GetString("server.external_url")
- url, err := url.Parse(externalUrl)
- if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.ServiceAuth).
- SetEndpoint(exception.EndpointAuthServiceMagic).
- SetType(exception.TypeSpecific).
- SetOriginal(exception.AuthMagicInvalidExternalUrl).
- SetError(err).
- Build(c)
- utils.HttpResponse(c, 500, errorCode)
- return
- }
-
- 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)
- url.RawQuery = query.Encode()
-
- debugMode := viper.GetBool("server.debug_mode")
- if debugMode {
- uriData := struct {
- Uri string `json:"uri"`
- }{url.String()}
- utils.HttpResponse(c, 200, "", "magiclink sent", uriData)
- return
- } else {
- // Send email using resend
- emailClient, err := new(email.Client).NewSMTPClient()
- if err != nil {
- errorCode := new(exception.Builder).
- SetStatus(exception.StatusServer).
- SetService(exception.ServiceAuth).
- SetEndpoint(exception.EndpointAuthServiceMagic).
- SetType(exception.TypeSpecific).
- SetOriginal(exception.AuthMagicInvalidEmailConfig).
- SetError(err).
- Build(c)
- utils.HttpResponse(c, 500, errorCode)
- return
- }
- emailClient.Send(
- "NixCN CMS ",
- req.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).
- SetStatus(exception.StatusServer).
- SetService(exception.ServiceAuth).
- SetEndpoint(exception.EndpointAuthServiceMagic).
- SetType(exception.TypeCommon).
- SetOriginal(exception.CommonSuccess).
- Build(c)
- utils.HttpResponse(c, 200, errorCode)
-}
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_agenda/agenda_test.go b/service/service_agenda/agenda_test.go
new file mode 100644
index 0000000..d7e63eb
--- /dev/null
+++ b/service/service_agenda/agenda_test.go
@@ -0,0 +1,611 @@
+package service_agenda
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/testutil"
+)
+
+func newAgendaSvc() AgendaService { return NewAgendaService() }
+
+// seed creates an event + user + attendance and returns them.
+func seedAgendaTestFixture(t *testing.T, ctx context.Context) (
+ event *data.Event,
+ user *data.User,
+ attendance *data.Attendance,
+) {
+ t.Helper()
+ user = testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, user.UserId, data.WithNickname("Attendee")))
+
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ event = data.NewEvent(
+ data.WithOwner(owner.UserId),
+ data.WithEventType("party"),
+ data.WithEventName("Test Agenda Event"),
+ data.WithEventSubtitle("subtitle"),
+ data.WithEventStartTime(time.Now().Add(48*time.Hour)),
+ data.WithEventEndTime(time.Now().Add(72*time.Hour)),
+ data.WithQuota(100),
+ data.WithLimit(150),
+ )
+ require.NoError(t, event.Create(ctx))
+
+ att := data.NewAttendance(
+ data.WithEventId(event.EventId),
+ data.WithUserId(user.UserId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := att.Create(ctx)
+ require.NoError(t, err)
+ attendance = att
+ return
+}
+
+// ---- Submit ----
+
+func TestAgendaSubmitSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+
+ svc := newAgendaSvc()
+ result := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{
+ EventId: event.EventId,
+ Name: "My Talk",
+ Description: "base64desc",
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.NotEqual(t, uuid.Nil, result.Data.AgendaId)
+}
+
+func TestAgendaSubmitNotAttendee(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, _, _ := seedAgendaTestFixture(t, ctx)
+ notAttendee := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+
+ svc := newAgendaSvc()
+ result := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: notAttendee.UserId,
+ Data: &SubmitData{
+ EventId: event.EventId,
+ Name: "Talk",
+ },
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+func TestAgendaSubmitEventStarted(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, user.UserId, data.WithNickname("Attendee")))
+
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ // Event that has already started
+ event := data.NewEvent(
+ data.WithOwner(owner.UserId),
+ data.WithEventType("party"),
+ data.WithEventName("Started Event"),
+ data.WithEventSubtitle("sub"),
+ data.WithEventStartTime(time.Now().Add(-time.Hour)),
+ data.WithEventEndTime(time.Now().Add(time.Hour)),
+ data.WithQuota(100),
+ data.WithLimit(150),
+ )
+ require.NoError(t, event.Create(ctx))
+
+ att := data.NewAttendance(
+ data.WithEventId(event.EventId),
+ data.WithUserId(user.UserId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := att.Create(ctx)
+ require.NoError(t, err)
+
+ svc := newAgendaSvc()
+ result := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{
+ EventId: event.EventId,
+ Name: "Late Talk",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+ assert.Equal(t, exception.AgendaSubmitEventStarted, result.Common.Exception.Original)
+}
+
+func TestAgendaSubmitMaxPendingLimit(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, attendance := seedAgendaTestFixture(t, ctx)
+
+ // Pre-fill 5 pending agendas directly
+ for i := 0; i < 5; i++ {
+ ag := data.NewAgenda(
+ data.WithAttendanceId(attendance.AttendanceId),
+ data.WithAgendaName(uuid.New().String()),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+ }
+
+ svc := newAgendaSvc()
+ result := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{
+ EventId: event.EventId,
+ Name: "One More",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+ assert.Equal(t, exception.AgendaSubmitPendingLimitReached, result.Common.Exception.Original)
+}
+
+// ---- Review ----
+
+func TestAgendaReviewApprove(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+
+ // Submit an agenda
+ svc := newAgendaSvc()
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "Talk"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+
+ result := svc.Review(&AgendaReviewPayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: submitResult.Data.AgendaId,
+ Status: "approved",
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+
+ ag, err := new(data.Agenda).GetByAgendaId(ctx, submitResult.Data.AgendaId)
+ require.NoError(t, err)
+ assert.Equal(t, "approved", ag.Status)
+}
+
+func TestAgendaReviewReject(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "Talk"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+
+ result := svc.Review(&AgendaReviewPayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: submitResult.Data.AgendaId,
+ Status: "rejected",
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestAgendaReviewInvalidStatus(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "Talk"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+
+ result := svc.Review(&AgendaReviewPayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: submitResult.Data.AgendaId,
+ Status: "banana",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+
+ // Verify the agenda status was not mutated in the database.
+ ag, err := new(data.Agenda).GetByAgendaId(ctx, submitResult.Data.AgendaId)
+ require.NoError(t, err)
+ assert.Equal(t, "pending", ag.Status)
+}
+
+// ---- List ----
+
+func TestAgendaList(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ for i := 0; i < 3; i++ {
+ r := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: uuid.New().String()},
+ })
+ require.Equal(t, 200, r.Common.HttpCode)
+ }
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ result := svc.List(&AgendaListPayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaListData{EventId: event.EventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Len(t, *result.Data, 3)
+}
+
+// ---- MyList ----
+
+func TestAgendaMyList(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ for i := 0; i < 2; i++ {
+ r := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: uuid.New().String()},
+ })
+ require.Equal(t, 200, r.Common.HttpCode)
+ }
+
+ result := svc.MyList(&AgendaMyListPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &AgendaMyListData{EventId: event.EventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Len(t, *result.Data, 2)
+}
+
+// ---- ScheduleGet ----
+
+func TestAgendaScheduleGetNotPublished(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, _, _ := seedAgendaTestFixture(t, ctx)
+
+ svc := newAgendaSvc()
+ result := svc.ScheduleGet(&AgendaScheduleGetPayload{
+ Context: ctx,
+ Data: &AgendaScheduleGetData{EventId: event.EventId},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+ assert.Equal(t, exception.AgendaScheduleGetNotPublished, result.Common.Exception.Original)
+}
+
+// ---- Update ----
+
+func TestAgendaUpdateSubmitterSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "Original Name"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ newName := "Updated Name"
+ result := svc.Update(&AgendaUpdatePayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &AgendaUpdateData{
+ AgendaId: submitResult.Data.AgendaId,
+ Name: &newName,
+ PermissionLevel: 10,
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+
+ ag, err := new(data.Agenda).GetByAgendaId(ctx, submitResult.Data.AgendaId)
+ require.NoError(t, err)
+ assert.Equal(t, "Updated Name", ag.Name)
+}
+
+func TestAgendaUpdateManagerCanEdit(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "Original Name"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ newName := "Manager Updated"
+ result := svc.Update(&AgendaUpdatePayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaUpdateData{
+ AgendaId: submitResult.Data.AgendaId,
+ Name: &newName,
+ PermissionLevel: 30,
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+
+ ag, err := new(data.Agenda).GetByAgendaId(ctx, submitResult.Data.AgendaId)
+ require.NoError(t, err)
+ assert.Equal(t, "Manager Updated", ag.Name)
+}
+
+func TestAgendaUpdateNotSubmitter(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "My Talk"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ other := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ newName := "Hijacked"
+ result := svc.Update(&AgendaUpdatePayload{
+ Context: ctx,
+ UserId: other.UserId,
+ Data: &AgendaUpdateData{
+ AgendaId: submitResult.Data.AgendaId,
+ Name: &newName,
+ PermissionLevel: 10,
+ },
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+ assert.Equal(t, exception.AgendaUpdateNotSubmitter, result.Common.Exception.Original)
+}
+
+func TestAgendaUpdateNotPending(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "My Talk"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ svc.Review(&AgendaReviewPayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: submitResult.Data.AgendaId,
+ Status: "approved",
+ },
+ })
+
+ newName := "Try to Edit"
+ result := svc.Update(&AgendaUpdatePayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &AgendaUpdateData{
+ AgendaId: submitResult.Data.AgendaId,
+ Name: &newName,
+ PermissionLevel: 10,
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+ assert.Equal(t, exception.AgendaUpdateNotPending, result.Common.Exception.Original)
+}
+
+func TestAgendaUpdateDeadlinePassed(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ user := testutil.SeedUser(t, testutil.RandomEmail(), 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, user.UserId, data.WithNickname("Attendee")))
+
+ owner := testutil.SeedUser(t, testutil.RandomEmail(), 40)
+ event := data.NewEvent(
+ data.WithOwner(owner.UserId),
+ data.WithEventType("party"),
+ data.WithEventName("Past Event"),
+ data.WithEventSubtitle("sub"),
+ data.WithEventStartTime(time.Now().Add(-2*time.Hour)),
+ data.WithEventEndTime(time.Now().Add(-time.Hour)),
+ data.WithQuota(100),
+ data.WithLimit(150),
+ )
+ require.NoError(t, event.Create(ctx))
+
+ att := data.NewAttendance(
+ data.WithEventId(event.EventId),
+ data.WithUserId(user.UserId),
+ data.WithKycId(uuid.Nil),
+ data.WithRole("attendee"),
+ data.WithState("success"),
+ )
+ _, err := att.Create(ctx)
+ require.NoError(t, err)
+
+ ag := data.NewAgenda(
+ data.WithAttendanceId(att.AttendanceId),
+ data.WithAgendaName("Past Talk"),
+ data.WithAgendaDescription("desc"),
+ data.WithAgendaStatus("pending"),
+ )
+ require.NoError(t, ag.Create(ctx))
+
+ newName := "Updated Past"
+ svc := newAgendaSvc()
+ result := svc.Update(&AgendaUpdatePayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &AgendaUpdateData{
+ AgendaId: ag.AgendaId,
+ Name: &newName,
+ PermissionLevel: 10,
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+ assert.Equal(t, exception.AgendaUpdateDeadlinePassed, result.Common.Exception.Original)
+}
+
+func TestAgendaUpdateNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newAgendaSvc()
+ newName := "Phantom"
+ result := svc.Update(&AgendaUpdatePayload{
+ Context: context.Background(),
+ UserId: uuid.New(),
+ Data: &AgendaUpdateData{
+ AgendaId: uuid.New(),
+ Name: &newName,
+ PermissionLevel: 10,
+ },
+ })
+
+ assert.Equal(t, 404, result.Common.HttpCode)
+}
+
+func TestAgendaScheduleGetPublished(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ event, user, _ := seedAgendaTestFixture(t, ctx)
+ svc := newAgendaSvc()
+
+ // Submit first (before publishing, so submission is allowed)
+ submitResult := svc.Submit(&SubmitPayload{
+ Context: ctx,
+ UserId: user.UserId,
+ Data: &SubmitData{EventId: event.EventId, Name: "Scheduled Talk"},
+ })
+ require.Equal(t, 200, submitResult.Common.HttpCode)
+
+ manager := testutil.SeedUser(t, testutil.RandomEmail(), 30)
+ reviewResult := svc.Review(&AgendaReviewPayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaReviewData{
+ EventId: event.EventId,
+ AgendaId: submitResult.Data.AgendaId,
+ Status: "approved",
+ },
+ })
+ require.Equal(t, 200, reviewResult.Common.HttpCode)
+
+ now := time.Now()
+ scheduleResult := svc.Schedule(&AgendaSchedulePayload{
+ Context: ctx,
+ UserId: manager.UserId,
+ Data: &AgendaScheduleData{
+ AgendaId: submitResult.Data.AgendaId,
+ StartTime: now.Add(time.Hour),
+ EndTime: now.Add(2 * time.Hour),
+ },
+ })
+ require.Equal(t, 200, scheduleResult.Common.HttpCode)
+
+ // Publish the agenda schedule so ScheduleGet returns it
+ require.NoError(t, new(data.Event).PatchByEventId(ctx, event.EventId,
+ data.WithIsAgendaPublished(true),
+ ))
+
+ result := svc.ScheduleGet(&AgendaScheduleGetPayload{
+ Context: ctx,
+ Data: &AgendaScheduleGetData{EventId: event.EventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Len(t, *result.Data, 1)
+}
diff --git a/service/service_agenda/list.go b/service/service_agenda/list.go
new file mode 100644
index 0000000..39b3f37
--- /dev/null
+++ b/service/service_agenda/list.go
@@ -0,0 +1,161 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type AgendaListData struct {
+ EventId uuid.UUID `json:"event_id" form:"event_id" validate:"required"`
+}
+
+type AgendaListPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AgendaListData
+}
+
+type AgendaUserProfile struct {
+ UserId uuid.UUID `json:"user_id"`
+ Nickname string `json:"nickname"`
+ Username string `json:"username"`
+}
+
+type AgendaListItem struct {
+ AgendaId uuid.UUID `json:"agenda_id"`
+ AttendanceId uuid.UUID `json:"attendance_id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Status string `json:"status"`
+ StartTime *time.Time `json:"start_time"`
+ EndTime *time.Time `json:"end_time"`
+ UserProfile *AgendaUserProfile `json:"user_profile"`
+}
+
+type AgendaListResult struct {
+ Common shared.CommonResult
+ Data *[]AgendaListItem
+}
+
+func (self *AgendaServiceImpl) List(payload *AgendaListPayload) (result *AgendaListResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaList)
+
+ eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &AgendaListResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ agendas, err := new(data.Agenda).GetListByEventId(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ attendanceIds := make([]uuid.UUID, 0, len(*agendas))
+ for _, a := range *agendas {
+ attendanceIds = append(attendanceIds, a.AttendanceId)
+ }
+
+ userProfiles, err := new(data.Attendance).GetUserProfilesByAttendanceIds(ctx, attendanceIds)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ zero := time.Time{}
+ items := make([]AgendaListItem, 0, len(*agendas))
+ for _, a := range *agendas {
+ profile := userProfiles[a.AttendanceId]
+
+ var startTime, endTime *time.Time
+ if a.StartTime != zero {
+ startTime = &a.StartTime
+ }
+ if a.EndTime != zero {
+ endTime = &a.EndTime
+ }
+
+ items = append(items, AgendaListItem{
+ AgendaId: a.AgendaId,
+ AttendanceId: a.AttendanceId,
+ Name: a.Name,
+ Description: a.Description,
+ Status: a.Status,
+ StartTime: startTime,
+ EndTime: endTime,
+ UserProfile: &AgendaUserProfile{
+ UserId: profile.UserId,
+ Nickname: profile.Nickname,
+ Username: profile.Username,
+ },
+ })
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaListResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: &items,
+ }
+ return
+}
diff --git a/service/service_agenda/my_list.go b/service/service_agenda/my_list.go
new file mode 100644
index 0000000..dab5b43
--- /dev/null
+++ b/service/service_agenda/my_list.go
@@ -0,0 +1,94 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type AgendaMyListData struct {
+ EventId uuid.UUID `json:"event_id" form:"event_id" validate:"required"`
+}
+
+type AgendaMyListPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AgendaMyListData
+}
+
+type AgendaMyListResult struct {
+ Common shared.CommonResult
+ Data *[]data.Agenda
+}
+
+func (self *AgendaServiceImpl) MyList(payload *AgendaMyListPayload) (result *AgendaMyListResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "my_list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaMyList)
+
+ attendanceData, err := new(data.Attendance).GetAttendance(ctx, payload.UserId, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaMyListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if attendanceData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("user is not an attendee of this event")),
+ ).Throw(ctx)
+
+ result = &AgendaMyListResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+
+ agendas, err := new(data.Agenda).GetListByAttendanceId(ctx, attendanceData.AttendanceId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaMyListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaMyListResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: agendas,
+ }
+ return
+}
diff --git a/service/service_agenda/review.go b/service/service_agenda/review.go
new file mode 100644
index 0000000..ec39ce1
--- /dev/null
+++ b/service/service_agenda/review.go
@@ -0,0 +1,152 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type AgendaReviewData struct {
+ AgendaId uuid.UUID `json:"agenda_id" validate:"required"`
+ EventId uuid.UUID `json:"event_id" validate:"required"`
+ Status string `json:"status" validate:"required"` // approved | rejected
+}
+
+type AgendaReviewPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AgendaReviewData
+}
+
+type AgendaReviewResult struct {
+ Common shared.CommonResult
+}
+
+func (self *AgendaServiceImpl) Review(payload *AgendaReviewPayload) (result *AgendaReviewResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "review",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaReview)
+
+ eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if eventData.IsAgendaPublished {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaReviewEventPublished),
+ exception.WithError(errors.New("cannot review agendas after agenda has been published")),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ agendaData, err := new(data.Agenda).GetByAgendaId(ctx, payload.Data.AgendaId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if agendaData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("agenda not found")),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if payload.Data.Status != "approved" && payload.Data.Status != "rejected" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("status must be approved or rejected")),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ if err := new(data.Agenda).PatchByAgendaId(ctx, payload.Data.AgendaId,
+ data.WithAgendaStatus(payload.Data.Status),
+ ); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaReviewResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+}
diff --git a/service/service_agenda/schedule.go b/service/service_agenda/schedule.go
new file mode 100644
index 0000000..9b6b80c
--- /dev/null
+++ b/service/service_agenda/schedule.go
@@ -0,0 +1,125 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type AgendaScheduleData struct {
+ AgendaId uuid.UUID `json:"agenda_id" validate:"required"`
+ StartTime time.Time `json:"start_time" validate:"required"`
+ EndTime time.Time `json:"end_time" validate:"required"`
+}
+
+type AgendaSchedulePayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AgendaScheduleData
+}
+
+type AgendaScheduleResult struct {
+ Common shared.CommonResult
+}
+
+func (self *AgendaServiceImpl) Schedule(payload *AgendaSchedulePayload) (result *AgendaScheduleResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "schedule",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaSchedule)
+
+ if !payload.Data.StartTime.Before(payload.Data.EndTime) {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("start_time must be before end_time")),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ agendaData, err := new(data.Agenda).GetByAgendaId(ctx, payload.Data.AgendaId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if agendaData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("agenda not found")),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if agendaData.Status != "approved" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaScheduleNotApproved),
+ exception.WithError(errors.New("only approved agendas can be scheduled")),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ if err := new(data.Agenda).PatchByAgendaId(ctx, payload.Data.AgendaId,
+ data.WithAgendaStartTime(payload.Data.StartTime),
+ data.WithAgendaEndTime(payload.Data.EndTime),
+ ); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+}
diff --git a/service/service_agenda/schedule_get.go b/service/service_agenda/schedule_get.go
new file mode 100644
index 0000000..404bcbc
--- /dev/null
+++ b/service/service_agenda/schedule_get.go
@@ -0,0 +1,107 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type AgendaScheduleGetData struct {
+ EventId uuid.UUID `json:"event_id" form:"event_id" validate:"required"`
+}
+
+type AgendaScheduleGetPayload struct {
+ Context context.Context
+ Data *AgendaScheduleGetData
+}
+
+type AgendaScheduleGetResult struct {
+ Common shared.CommonResult
+ Data *[]data.AgendaDoc
+}
+
+func (self *AgendaServiceImpl) ScheduleGet(payload *AgendaScheduleGetPayload) (result *AgendaScheduleGetResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "schedule_get",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaScheduleGet)
+
+ eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleGetResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleGetResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if !eventData.IsAgendaPublished {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaScheduleGetNotPublished),
+ exception.WithError(errors.New("agenda has not been published for this event")),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleGetResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+
+ agendas, err := new(data.Agenda).GetScheduledByEventId(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleGetResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaScheduleGetResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: agendas,
+ }
+ return
+}
diff --git a/service/service_agenda/service.go b/service/service_agenda/service.go
new file mode 100644
index 0000000..505e7c1
--- /dev/null
+++ b/service/service_agenda/service.go
@@ -0,0 +1,17 @@
+package service_agenda
+
+type AgendaService interface {
+ Submit(*SubmitPayload) *SubmitResult
+ Update(*AgendaUpdatePayload) *AgendaUpdateResult
+ Review(*AgendaReviewPayload) *AgendaReviewResult
+ Schedule(*AgendaSchedulePayload) *AgendaScheduleResult
+ List(*AgendaListPayload) *AgendaListResult
+ MyList(*AgendaMyListPayload) *AgendaMyListResult
+ ScheduleGet(*AgendaScheduleGetPayload) *AgendaScheduleGetResult
+}
+
+type AgendaServiceImpl struct{}
+
+func NewAgendaService() AgendaService {
+ return &AgendaServiceImpl{}
+}
diff --git a/service/service_agenda/submit.go b/service/service_agenda/submit.go
new file mode 100644
index 0000000..6db88a6
--- /dev/null
+++ b/service/service_agenda/submit.go
@@ -0,0 +1,201 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type SubmitData struct {
+ EventId uuid.UUID `json:"event_id" validate:"required"`
+ Name string `json:"name" validate:"required"`
+ Description string `json:"description" validate:"required"`
+}
+
+type SubmitPayload struct {
+ Context context.Context
+ UserId uuid.UUID `json:"user_id"`
+ Data *SubmitData
+}
+
+type SubmitResponse struct {
+ AgendaId uuid.UUID `json:"agenda_id"`
+}
+
+type SubmitResult struct {
+ Common shared.CommonResult
+ Data *SubmitResponse
+}
+
+func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "submit",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaSubmit)
+
+ attendanceData, err := new(data.Attendance).
+ GetAttendance(ctx, payload.UserId, payload.Data.EventId)
+
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if attendanceData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("user is not an attendee of this event")),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+
+ eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if eventData.StartTime.Before(time.Now()) {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaSubmitEventStarted),
+ exception.WithError(errors.New("cannot submit agenda after event has started")),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ if eventData.IsAgendaPublished {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaSubmitEventPublished),
+ exception.WithError(errors.New("cannot submit agenda after agenda has been published")),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ pendingCount, err := new(data.Agenda).CountPendingByAttendanceId(ctx, attendanceData.AttendanceId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if pendingCount >= 5 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaSubmitPendingLimitReached),
+ exception.WithError(errors.New("maximum of 5 pending agendas allowed")),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ agendaModel := data.NewAgenda(
+ data.WithAttendanceId(attendanceData.AttendanceId),
+ data.WithAgendaName(payload.Data.Name),
+ data.WithAgendaDescription(payload.Data.Description),
+ data.WithAgendaStatus("pending"),
+ )
+
+ if err = agendaModel.Create(ctx); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &SubmitResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &SubmitResponse{
+ AgendaId: agendaModel.AgendaId,
+ },
+ }
+
+ return
+}
diff --git a/service/service_agenda/update.go b/service/service_agenda/update.go
new file mode 100644
index 0000000..8797cbf
--- /dev/null
+++ b/service/service_agenda/update.go
@@ -0,0 +1,192 @@
+package service_agenda
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type AgendaUpdateData struct {
+ AgendaId uuid.UUID `json:"agenda_id" validate:"required"`
+ Name *string `json:"name"`
+ Description *string `json:"description"`
+ PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
+}
+
+type AgendaUpdatePayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AgendaUpdateData
+}
+
+type AgendaUpdateResult struct {
+ Common shared.CommonResult
+}
+
+func (self *AgendaServiceImpl) Update(payload *AgendaUpdatePayload) (result *AgendaUpdateResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_agenda",
+ "update",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAgendaUpdate)
+
+ agendaData, err := new(data.Agenda).GetByAgendaId(ctx, payload.Data.AgendaId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if agendaData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("agenda not found")),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ isManager := payload.Data.PermissionLevel >= 30
+
+ if !isManager {
+ myAttendance, err := new(data.Attendance).GetAttendanceByAttendanceId(ctx, agendaData.AttendanceId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if myAttendance == nil || myAttendance.UserId != payload.UserId {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaUpdateNotSubmitter),
+ exception.WithError(errors.New("you are not the submitter of this agenda")),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+
+ if agendaData.Status != "pending" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaUpdateNotPending),
+ exception.WithError(errors.New("submitters may only edit pending agendas")),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ eventData, err := new(data.Event).GetEventById(ctx, myAttendance.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData != nil && eventData.StartTime.Before(time.Now()) {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AgendaUpdateDeadlinePassed),
+ exception.WithError(errors.New("submission deadline has passed")),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ }
+
+ var opts []data.AgendaOption
+ if payload.Data.Name != nil {
+ opts = append(opts, data.WithAgendaName(*payload.Data.Name))
+ }
+ if payload.Data.Description != nil {
+ opts = append(opts, data.WithAgendaDescription(*payload.Data.Description))
+ }
+
+ if len(opts) == 0 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+ }
+
+ if err := new(data.Agenda).PatchByAgendaId(ctx, payload.Data.AgendaId, opts...); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AgendaUpdateResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+}
diff --git a/service/service_auth/auth_test.go b/service/service_auth/auth_test.go
new file mode 100644
index 0000000..012b3e4
--- /dev/null
+++ b/service/service_auth/auth_test.go
@@ -0,0 +1,328 @@
+package service_auth
+
+import (
+ "context"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/testutil"
+)
+
+func newSvc() AuthService { return NewAuthService() }
+
+func parseURI(t *testing.T, raw string) *url.URL {
+ t.Helper()
+ u, err := url.Parse(raw)
+ require.NoError(t, err)
+ return u
+}
+
+type tokenPair struct {
+ AccessToken string
+ RefreshToken string
+}
+
+// getTokensForUser runs the full magic → redirect → token flow and
+// returns the issued token pair.
+func getTokensForUser(t *testing.T, ctx context.Context, email string) tokenPair {
+ t.Helper()
+ svc := newSvc()
+
+ magicResult := svc.Magic(&MagicPayload{
+ Context: ctx,
+ Data: &MagicData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Email: email,
+ },
+ })
+ require.Equal(t, 200, magicResult.Common.HttpCode)
+ magicURI := parseURI(t, magicResult.Data.Uri)
+
+ redirectResult := svc.Redirect(&RedirectPayload{
+ Context: ctx,
+ Data: &RedirectData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Code: magicURI.Query().Get("code"),
+ },
+ })
+ require.Equal(t, 200, redirectResult.Common.HttpCode)
+
+ finalURI := parseURI(t, redirectResult.Data)
+ tokenResult := svc.Token(&TokenPayload{
+ Context: ctx,
+ Data: &TokenData{Code: finalURI.Query().Get("code")},
+ })
+ require.Equal(t, 200, tokenResult.Common.HttpCode)
+
+ return tokenPair{
+ AccessToken: tokenResult.Data.AccessToken,
+ RefreshToken: tokenResult.Data.RefreshToken,
+ }
+}
+
+// ---- Exchange ----
+
+func TestExchangeSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "exchange@example.com", 10)
+ testutil.SeedClient(t)
+
+ svc := newSvc()
+ result := svc.Exchange(&ExchangePayload{
+ Context: ctx,
+ UserId: u.UserId,
+ Data: &ExchangeData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "state123",
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Contains(t, result.Data.RedirectUri, "code=")
+}
+
+func TestExchangeInvalidRedirectUri(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "exchange2@example.com", 10)
+ testutil.SeedClient(t)
+
+ svc := newSvc()
+ result := svc.Exchange(&ExchangePayload{
+ Context: ctx,
+ UserId: u.UserId,
+ Data: &ExchangeData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "://invalid-url",
+ State: "s",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestExchangeUserNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ svc := newSvc()
+ result := svc.Exchange(&ExchangePayload{
+ Context: ctx,
+ UserId: uuid.New(),
+ Data: &ExchangeData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ },
+ })
+
+ assert.Equal(t, 500, result.Common.HttpCode)
+}
+
+// ---- Magic (debug_mode = true, skips Turnstile and Email) ----
+
+func TestMagicDebugMode(t *testing.T) {
+ testutil.Setup(t) // sets server.debug_mode = true
+ ctx := context.Background()
+
+ svc := newSvc()
+ result := svc.Magic(&MagicPayload{
+ Context: ctx,
+ Data: &MagicData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "state",
+ Email: "magic@example.com",
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Contains(t, result.Data.Uri, "/app/api/v1/auth/redirect")
+}
+
+// ---- Redirect ----
+
+func TestRedirectCreatesNewUser(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ testutil.SeedClient(t)
+
+ svc := newSvc()
+ magicResult := svc.Magic(&MagicPayload{
+ Context: ctx,
+ Data: &MagicData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Email: "newuser@example.com",
+ },
+ })
+ require.Equal(t, 200, magicResult.Common.HttpCode)
+ require.NotNil(t, magicResult.Data)
+
+ u := parseURI(t, magicResult.Data.Uri)
+ code := u.Query().Get("code")
+ require.NotEmpty(t, code)
+
+ result := svc.Redirect(&RedirectPayload{
+ Context: ctx,
+ Data: &RedirectData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Code: code,
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ assert.Contains(t, result.Data, "code=")
+
+ email := "newuser@example.com"
+ user, err := new(data.User).GetByEmail(ctx, &email)
+ require.NoError(t, err)
+ assert.Equal(t, "newuser@example.com", user.Email)
+}
+
+func TestRedirectInvalidCode(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newSvc()
+ result := svc.Redirect(&RedirectPayload{
+ Context: context.Background(),
+ Data: &RedirectData{
+ ClientId: testutil.TestClientID,
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Code: "bad-code",
+ },
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+func TestRedirectClientNotFound(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ svc := newSvc()
+ magicResult := svc.Magic(&MagicPayload{
+ Context: ctx,
+ Data: &MagicData{
+ ClientId: "no-such-client",
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Email: "x@example.com",
+ },
+ })
+ require.Equal(t, 200, magicResult.Common.HttpCode)
+
+ u := parseURI(t, magicResult.Data.Uri)
+ code := u.Query().Get("code")
+
+ result := svc.Redirect(&RedirectPayload{
+ Context: ctx,
+ Data: &RedirectData{
+ ClientId: "no-such-client",
+ RedirectUri: "http://localhost/callback",
+ State: "s",
+ Code: code,
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+ assert.Equal(t, exception.AuthRedirectClientNotFound, result.Common.Exception.Original)
+}
+
+// ---- Token ----
+
+func TestTokenSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ testutil.SeedClient(t)
+ tokens := getTokensForUser(t, ctx, "token@example.com")
+
+ assert.NotEmpty(t, tokens.AccessToken)
+ assert.NotEmpty(t, tokens.RefreshToken)
+}
+
+func TestTokenInvalidCode(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newSvc()
+ result := svc.Token(&TokenPayload{
+ Context: context.Background(),
+ Data: &TokenData{Code: "invalid"},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+// ---- Refresh ----
+
+func TestRefreshSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ testutil.SeedClient(t)
+ tokens := getTokensForUser(t, ctx, "refresh@example.com")
+
+ svc := newSvc()
+ result := svc.Refresh(&RefreshPayload{
+ Context: ctx,
+ Data: &RefreshData{RefreshToken: tokens.RefreshToken},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.NotEmpty(t, result.Data.AccessToken)
+ assert.NotEmpty(t, result.Data.RefreshToken)
+ assert.NotEqual(t, tokens.RefreshToken, result.Data.RefreshToken)
+}
+
+func TestRefreshInvalidToken(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newSvc()
+ result := svc.Refresh(&RefreshPayload{
+ Context: context.Background(),
+ Data: &RefreshData{RefreshToken: "bad-refresh"},
+ })
+
+ assert.Equal(t, 401, result.Common.HttpCode)
+}
+
+func TestRefreshTokenExpires(t *testing.T) {
+ mr := testutil.Setup(t)
+ ctx := context.Background()
+ testutil.SeedClient(t)
+
+ tokens := getTokensForUser(t, ctx, "expire@example.com")
+
+ mr.FastForward(7*24*time.Hour + time.Minute)
+
+ svc := newSvc()
+ result := svc.Refresh(&RefreshPayload{
+ Context: ctx,
+ Data: &RefreshData{RefreshToken: tokens.RefreshToken},
+ })
+
+ assert.Equal(t, 401, result.Common.HttpCode)
+}
diff --git a/service/service_auth/exchange.go b/service/service_auth/exchange.go
new file mode 100644
index 0000000..18019ff
--- /dev/null
+++ b/service/service_auth/exchange.go
@@ -0,0 +1,130 @@
+package service_auth
+
+import (
+ "context"
+ "net/url"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/authcode"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type ExchangeData struct {
+ ClientId string `json:"client_id" validate:"required"`
+ RedirectUri string `json:"redirect_uri" validate:"required"`
+ State string `json:"state"`
+}
+
+type ExchangePayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *ExchangeData
+}
+
+type ExchangeResponse struct {
+ RedirectUri string `json:"redirect_uri" validate:"required"`
+}
+
+type ExchangeResult struct {
+ Common shared.CommonResult
+ Data *ExchangeResponse
+}
+
+func (self *AuthServiceImpl) Exchange(payload *ExchangePayload) (result *ExchangeResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_auth",
+ "exchange",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAuthExchange)
+
+ var err error
+
+ userData, err := new(data.User).
+ GetByUserId(ctx, &payload.UserId)
+ if err != nil || userData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthExchangeGetUserIdFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &ExchangeResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ code, err := authcode.NewAuthCode(ctx, payload.Data.ClientId, userData.Email)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthExchangeCodeGenFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &ExchangeResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ url, err := url.Parse(payload.Data.RedirectUri)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthExchangeInvalidRedirectUri),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &ExchangeResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ query := url.Query()
+ query.Set("code", code)
+ url.RawQuery = query.Encode()
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ resultData := &ExchangeResponse{url.String()}
+
+ result = &ExchangeResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: resultData,
+ }
+
+ return
+}
diff --git a/service/service_auth/magic.go b/service/service_auth/magic.go
new file mode 100644
index 0000000..4847c27
--- /dev/null
+++ b/service/service_auth/magic.go
@@ -0,0 +1,190 @@
+package service_auth
+
+import (
+ "context"
+ "net/url"
+ "nixcn-cms/internal/authcode"
+ "nixcn-cms/internal/email"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/internal/turnstile"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/spf13/viper"
+)
+
+type MagicData struct {
+ ClientId string `json:"client_id" validate:"required"`
+ RedirectUri string `json:"redirect_uri" validate:"required"`
+ State string `json:"state"`
+ Email string `json:"email" validate:"required"`
+ TurnstileToken string `json:"turnstile_token" validate:"required"`
+ ClientIP string `json:"client_ip"`
+}
+
+type MagicPayload struct {
+ Context context.Context
+ Data *MagicData
+}
+
+type MagicResponse struct {
+ Uri string `json:"uri" validate:"required"`
+}
+
+type MagicResult struct {
+ Common shared.CommonResult
+ Data *MagicResponse
+}
+
+func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_auth",
+ "magic",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAuthMagic)
+
+ var ok bool
+ var err error
+
+ if viper.GetBool("server.debug_mode") {
+ goto BypassCfTurnstile
+ }
+
+ ok, err = turnstile.VerifyTurnstile(payload.Data.TurnstileToken, payload.Data.ClientIP)
+ if err != nil || !ok {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthMagicTurnstileFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &MagicResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+BypassCfTurnstile:
+ code, err := authcode.NewAuthCode(ctx, payload.Data.ClientId, payload.Data.Email)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthMagicCodeGenFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &MagicResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ externalUrl := viper.GetString("server.external_url")
+ url, err := url.Parse(externalUrl)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthMagicInvalidExternalUrl),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &MagicResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ url.Path = "/app/api/v1/auth/redirect"
+ query := url.Query()
+ query.Set("code", code)
+ 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")
+ if debugMode {
+ uriData := struct {
+ Uri string `json:"uri"`
+ }{url.String()}
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &MagicResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &MagicResponse{uriData.Uri},
+ }
+
+ return
+ } else {
+ emailClient, err := new(email.Client).NewSMTPClient()
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthMagicInvalidEmailConfig),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &MagicResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+ emailClient.Send(
+ "NixCN CMS ",
+ payload.Data.Email,
+ "NixCN CMS Email Verify",
+ "Click the link below to verify your email. This link will expire in 10 minutes.
"+url.String()+"",
+ )
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &MagicResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+}
diff --git a/service/service_auth/redirect.go b/service/service_auth/redirect.go
new file mode 100644
index 0000000..28260b7
--- /dev/null
+++ b/service/service_auth/redirect.go
@@ -0,0 +1,207 @@
+package service_auth
+
+import (
+ "context"
+ "errors"
+ "net/url"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/authcode"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type RedirectData struct {
+ ClientId string `json:"client_id" validate:"required"`
+ RedirectUri string `json:"redirect_uri" validate:"required"`
+ State string `json:"state"`
+ Code string `json:"code" validate:"required"`
+}
+
+type RedirectPayload struct {
+ Context context.Context
+ Data *RedirectData
+}
+
+type RedirectResult struct {
+ Common shared.CommonResult
+ Data string
+}
+
+func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *RedirectResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_auth",
+ "redirect",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAuthRedirect)
+
+ var err error
+
+ authCode, ok := authcode.VerifyAuthCode(ctx, payload.Data.Code)
+ if !ok {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthRedirectTokenInvalid),
+ exception.WithError(errors.New("Invalid Auth Code")),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ userData, err := new(data.User).
+ GetByEmail(ctx, &authCode.Email)
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ newUserId := uuid.New()
+ userData = data.NewUser(
+ data.WithEmail(authCode.Email),
+ data.WithUsername(newUserId.String()),
+ data.WithPermissionLevel(10),
+ )
+ if err := userData.Create(ctx); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+ } else {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+ }
+
+ clientData := new(data.Client)
+ client, err := clientData.GetClientByClientId(ctx, payload.Data.ClientId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthRedirectClientNotFound),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ if err = client.ValidateRedirectURI(payload.Data.RedirectUri); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthRedirectUriMismatch),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ newCode, err := authcode.NewAuthCode(ctx, payload.Data.ClientId, authCode.Email)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ targetUrl, err := url.Parse(payload.Data.RedirectUri)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthRedirectInvalidUri),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RedirectResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ }
+
+ 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: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ Data: targetUrl.String(),
+ }
+
+ return
+}
diff --git a/service/service_auth/refresh.go b/service/service_auth/refresh.go
new file mode 100644
index 0000000..08be44b
--- /dev/null
+++ b/service/service_auth/refresh.go
@@ -0,0 +1,103 @@
+package service_auth
+
+import (
+ "context"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/spf13/viper"
+)
+
+type RefreshData struct {
+ RefreshToken string `json:"refresh_token" validate:"required"`
+}
+
+type RefreshPayload struct {
+ Context context.Context
+ Data *RefreshData
+}
+
+type RefreshResult struct {
+ Common shared.CommonResult
+ Data *TokenResponse
+}
+
+func (self *AuthServiceImpl) Refresh(payload *RefreshPayload) (result *RefreshResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_auth",
+ "refresh",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAuthRefresh)
+
+ var err error
+ JwtTool := authtoken.Token{
+ Application: viper.GetString("server.application"),
+ }
+
+ // 1. Refresh Access Token
+ accessToken, err := JwtTool.RefreshAccessToken(ctx, payload.Data.RefreshToken)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthRefreshInvalidToken),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RefreshResult{
+ Common: shared.CommonResult{
+ HttpCode: 401,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ // 2. Renew Refresh Token (Rotation)
+ refreshToken, err := JwtTool.RenewRefreshToken(ctx, payload.Data.RefreshToken)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthRefreshRenewFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &RefreshResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ // 3. Success Assignment
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &RefreshResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &TokenResponse{
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ },
+ }
+
+ return
+}
diff --git a/service/service_auth/service.go b/service/service_auth/service.go
new file mode 100644
index 0000000..8bc5beb
--- /dev/null
+++ b/service/service_auth/service.go
@@ -0,0 +1,15 @@
+package service_auth
+
+type AuthService interface {
+ Exchange(*ExchangePayload) *ExchangeResult
+ Magic(*MagicPayload) *MagicResult
+ Redirect(*RedirectPayload) *RedirectResult
+ Token(*TokenPayload) *TokenResult
+ Refresh(*RefreshPayload) *RefreshResult
+}
+
+type AuthServiceImpl struct{}
+
+func NewAuthService() AuthService {
+ return &AuthServiceImpl{}
+}
diff --git a/service/service_auth/token.go b/service/service_auth/token.go
new file mode 100644
index 0000000..3729982
--- /dev/null
+++ b/service/service_auth/token.go
@@ -0,0 +1,123 @@
+package service_auth
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/authcode"
+ "nixcn-cms/internal/authtoken"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/spf13/viper"
+)
+
+type TokenData struct {
+ Code string `json:"code" validate:"required"`
+}
+
+type TokenPayload struct {
+ Context context.Context
+ Data *TokenData
+}
+
+type TokenResult struct {
+ Common shared.CommonResult
+ Data *TokenResponse
+}
+
+type TokenResponse struct {
+ AccessToken string `json:"access_token" validate:"required"`
+ RefreshToken string `json:"refresh_token" validate:"required"`
+}
+
+func (self *AuthServiceImpl) Token(payload *TokenPayload) (result *TokenResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_auth",
+ "token",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceAuthToken)
+
+ var err error
+ authCode, ok := authcode.VerifyAuthCode(ctx, payload.Data.Code)
+ if !ok {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthTokenInvalidToken),
+ exception.WithError(errors.New("Invalid Auth Code")),
+ ).Throw(ctx)
+
+ result = &TokenResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ userData := new(data.User)
+ user, err := userData.GetByEmail(ctx, &authCode.Email)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &TokenResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ JwtTool := authtoken.Token{
+ Application: viper.GetString("server.application"),
+ }
+ accessToken, refreshToken, err := JwtTool.IssueTokens(ctx, authCode.ClientId, user.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.AuthTokenGenFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &TokenResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ result = &TokenResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ Data: &TokenResponse{
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ },
+ }
+
+ return
+}
diff --git a/service/service_event/attendance_list.go b/service/service_event/attendance_list.go
new file mode 100644
index 0000000..f845f4c
--- /dev/null
+++ b/service/service_event/attendance_list.go
@@ -0,0 +1,318 @@
+package service_event
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/cryptography"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/internal/kyc"
+ "nixcn-cms/service/service_user"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "strconv"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/spf13/viper"
+)
+
+type AttendanceListData struct {
+ EventId uuid.UUID `json:"event_id" form:"event_id" validate:"required"`
+ Name string `json:"name" form:"name"`
+ KycStatus string `json:"kyc_status" form:"kyc_status"` // "with_kyc" | "without_kyc" | ""
+ Limit *string `json:"limit" form:"limit"`
+ Offset *string `json:"offset" form:"offset"`
+ SortBy *string `json:"sort_by" form:"sort_by"`
+ SortOrder *string `json:"sort_order" form:"sort_order"`
+}
+
+type AttendanceListPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AttendanceListData
+}
+
+type AttendanceListResponse struct {
+ AttendanceId string `json:"attendance_id" validate:"required"`
+ UserInfo service_user.UserInfoData `json:"user_info" validate:"required"`
+ KycStatus string `json:"kyc_status"`
+ KycType string `json:"kyc_type"`
+ KycInfo any `json:"kyc_info"`
+ JoinedAt *time.Time `json:"joined_at"`
+ CheckedInAt *time.Time `json:"checked_in_at"`
+}
+
+type AttendanceListPagedResponse struct {
+ Total int64 `json:"total"`
+ Items []AttendanceListResponse `json:"items"`
+}
+
+type AttendanceListResult struct {
+ Common shared.CommonResult
+ Data *AttendanceListPagedResponse
+}
+
+func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (result *AttendanceListResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "attendance_list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventAttendanceList)
+
+ eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if eventData.Owner != payload.UserId {
+ callerData, err := new(data.User).GetByUserId(ctx, &payload.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if callerData == nil || callerData.PermissionLevel < 40 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventAttendanceListError),
+ exception.WithError(errors.New("only the event owner may view the attendance list")),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+ }
+
+ limit := 20
+ if payload.Data.Limit != nil && *payload.Data.Limit != "" {
+ if v, err := strconv.Atoi(*payload.Data.Limit); err == nil {
+ limit = v
+ }
+ }
+
+ offset := 0
+ if payload.Data.Offset != nil && *payload.Data.Offset != "" {
+ if v, err := strconv.Atoi(*payload.Data.Offset); err == nil {
+ offset = v
+ }
+ }
+
+ sortBy := ""
+ if payload.Data.SortBy != nil {
+ sortBy = *payload.Data.SortBy
+ }
+ sortOrder := ""
+ if payload.Data.SortOrder != nil {
+ sortOrder = *payload.Data.SortOrder
+ }
+
+ filter := data.AttendanceListFilter{
+ EventId: payload.Data.EventId,
+ Name: payload.Data.Name,
+ KycStatus: payload.Data.KycStatus,
+ SortBy: sortBy,
+ SortOrder: sortOrder,
+ Limit: limit,
+ Offset: offset,
+ }
+
+ attList, total, err := new(data.Attendance).GetAttendanceListFiltered(ctx, filter)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventAttendanceListError),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ responseList := make([]AttendanceListResponse, 0)
+ kycRepo := new(data.Kyc)
+ userRepo := new(data.User)
+
+ for _, item := range *attList {
+ var userInfo service_user.UserInfoData
+ var kycStatus string
+ var kycType string
+ var kycInfo any
+
+ if item.KycId != uuid.Nil {
+ kycStatus = "with_kyc"
+ } else {
+ kycStatus = "without_kyc"
+ }
+
+ userData, err := userRepo.GetByUserId(ctx, &item.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if userData != nil {
+ userInfo = service_user.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,
+ }
+ }
+
+ if item.KycId != uuid.Nil {
+ kycData, err := kycRepo.GetByKycId(ctx, &item.KycId)
+
+ if err == nil && kycData != nil {
+ kycType = kycData.Type
+
+ decodedKycInfo, err := cryptography.AESCBCDecrypt(string(kycData.KycInfo), []byte(viper.GetString("secrets.kyc_info_key")))
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorAesDecodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ switch kycType {
+ case "cnrid":
+ var kycDetail kyc.CNRidInfo
+ if err := json.Unmarshal(decodedKycInfo, &kycDetail); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorJsonDecodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+ kycInfo = kycDetail
+
+ case "passport":
+ var kycDetail kyc.PassportResp
+ if err := json.Unmarshal(decodedKycInfo, &kycDetail); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorJsonDecodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+ kycInfo = kycDetail
+
+ default:
+ }
+ }
+ }
+
+ var joinedAt *time.Time
+ if !item.JoinedAt.IsZero() {
+ t := item.JoinedAt
+ joinedAt = &t
+ }
+
+ var checkedInAt *time.Time
+ if !item.CheckinAt.IsZero() {
+ t := item.CheckinAt
+ checkedInAt = &t
+ }
+
+ responseList = append(responseList, AttendanceListResponse{
+ AttendanceId: item.AttendanceId.String(),
+ UserInfo: userInfo,
+ KycStatus: kycStatus,
+ KycType: kycType,
+ KycInfo: kycInfo,
+ JoinedAt: joinedAt,
+ CheckedInAt: checkedInAt,
+ })
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &AttendanceListResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: &AttendanceListPagedResponse{
+ Total: total,
+ Items: responseList,
+ },
+ }
+ return
+}
diff --git a/service/service_event/checkin.go b/service/service_event/checkin.go
new file mode 100644
index 0000000..fc9f478
--- /dev/null
+++ b/service/service_event/checkin.go
@@ -0,0 +1,283 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type CheckinData struct {
+ EventId uuid.UUID `json:"event_id" validate:"required"`
+}
+
+type CheckinPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *CheckinData
+}
+
+type CheckinResponse struct {
+ CheckinCode *string `json:"checkin_code" validate:"required"`
+}
+
+type CheckinResult struct {
+ Common shared.CommonResult
+ Data *CheckinResponse
+}
+
+func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "checkin",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventCheckin)
+
+ var err error
+ attendandeData, err := new(data.Attendance).
+ GetAttendance(ctx, payload.UserId, payload.Data.EventId)
+ if err != nil {
+ result = &CheckinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx),
+ },
+ }
+ return
+ }
+
+ if attendandeData == nil {
+ result = &CheckinResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("attendance record not found")),
+ ).Throw(ctx),
+ },
+ }
+ return
+ }
+
+ eventData, err := new(data.Event).
+ GetEventById(ctx, payload.Data.EventId)
+
+ if err != nil || eventData == nil {
+ code := 500
+ if eventData == nil {
+ code = 404
+ }
+ result = &CheckinResult{
+ Common: shared.CommonResult{
+ HttpCode: code,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx),
+ },
+ }
+ return
+ }
+
+ if attendandeData.KycId == uuid.Nil && eventData.EnableKYC == true {
+ result = &CheckinResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("KYC required")),
+ ).Throw(ctx),
+ },
+ }
+ return
+ }
+
+ attendance := data.NewAttendance(data.WithUserId(payload.UserId))
+ code, err := attendance.GenCheckinCode(ctx, payload.Data.EventId)
+ if err != nil {
+ result = &CheckinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventCheckinGenCodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx),
+ },
+ }
+
+ return
+ }
+
+ result = &CheckinResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ Data: &CheckinResponse{code},
+ }
+
+ return
+}
+
+type CheckinSubmitData struct {
+ CheckinCode string `json:"checkin_code" validate:"required"`
+}
+
+type CheckinSubmitPayload struct {
+ Context context.Context
+ Data *CheckinSubmitData
+}
+
+type CheckinSubmitResult struct {
+ Common shared.CommonResult
+}
+
+func (self *EventServiceImpl) CheckinSubmit(payload *CheckinSubmitPayload) (result *CheckinSubmitResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "checkin_submit",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventCheckin)
+
+ var err error
+ attendanceData := new(data.Attendance)
+ err = attendanceData.VerifyCheckinCode(ctx, payload.Data.CheckinCode)
+ if err != nil {
+ result = &CheckinSubmitResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx),
+ },
+ }
+ return
+ }
+
+ result = &CheckinSubmitResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ }
+ return
+}
+
+type CheckinQueryData struct {
+ EventId uuid.UUID `json:"event_id" validate:"required"`
+}
+
+type CheckinQueryPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *CheckinQueryData
+}
+
+type CheckinQueryResponse struct {
+ CheckinAt *time.Time `json:"checkin_at"`
+}
+
+type CheckinQueryResult struct {
+ Common shared.CommonResult
+ Data *CheckinQueryResponse
+}
+
+func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result *CheckinQueryResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "checkin_query",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventCheckin)
+
+ var err error
+ attendanceData := new(data.Attendance)
+ attendance, err := attendanceData.GetAttendance(ctx, payload.UserId, payload.Data.EventId)
+
+ if err != nil {
+ result = &CheckinQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx),
+ },
+ Data: nil,
+ }
+ return
+ }
+
+ if attendance == nil {
+ result = &CheckinQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 404,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventCheckinQueryRecordNotFound),
+ exception.WithError(errors.New("attendance record not found")),
+ ).Throw(ctx),
+ },
+ }
+ return
+ }
+
+ var checkinAt *time.Time
+ if !attendance.CheckinAt.IsZero() {
+ checkinAt = &attendance.CheckinAt
+ }
+
+ result = &CheckinQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ Data: &CheckinQueryResponse{checkinAt},
+ }
+
+ return
+}
diff --git a/service/service_event/create.go b/service/service_event/create.go
new file mode 100644
index 0000000..20dd6b8
--- /dev/null
+++ b/service/service_event/create.go
@@ -0,0 +1,160 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type EventCreateData struct {
+ Type string `json:"type" validate:"required"`
+ EnableKYC bool `json:"enable_kyc"`
+ Name string `json:"name" validate:"required"`
+ Subtitle string `json:"subtitle"`
+ Description string `json:"description"`
+ AttendanceGuide string `json:"attendance_guide"`
+ StartTime time.Time `json:"start_time"`
+ EndTime time.Time `json:"end_time"`
+ Thumbnail string `json:"thumbnail"`
+ Quota int64 `json:"quota"`
+ Limit int64 `json:"limit"`
+ UserId string `json:"user_id" swaggerignore:"true"`
+ PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
+}
+
+type EventCreatePayload struct {
+ Context context.Context
+ Data *EventCreateData
+}
+
+type EventCreateResponse struct {
+ EventId string `json:"event_id" validate:"required"`
+}
+
+type EventCreateResult struct {
+ Common shared.CommonResult
+ Data *EventCreateResponse
+}
+
+func (self *EventServiceImpl) Create(payload *EventCreatePayload) (result *EventCreateResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "create",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventCreate)
+
+ if payload.Data.Type != "party" && payload.Data.Type != "official" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("type must be 'party' or 'official'")),
+ ).Throw(ctx)
+
+ result = &EventCreateResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ if payload.Data.PermissionLevel == 30 && payload.Data.Type != "party" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventCreateTypeNotAllowed),
+ exception.WithError(errors.New("Lv30 users may only create events with type 'party'")),
+ ).Throw(ctx)
+
+ result = &EventCreateResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ ownerId, err := uuid.Parse(payload.Data.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventCreateResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ eventData := data.NewEvent(
+ data.WithEventType(payload.Data.Type),
+ data.WithEnableKYC(payload.Data.EnableKYC),
+ data.WithEventName(payload.Data.Name),
+ data.WithEventSubtitle(payload.Data.Subtitle),
+ data.WithEventDescription(payload.Data.Description),
+ data.WithAttendanceGuide(payload.Data.AttendanceGuide),
+ data.WithEventStartTime(payload.Data.StartTime),
+ data.WithEventEndTime(payload.Data.EndTime),
+ data.WithThumbnail(payload.Data.Thumbnail),
+ data.WithQuota(payload.Data.Quota),
+ data.WithLimit(payload.Data.Limit),
+ data.WithOwner(ownerId),
+ )
+
+ if err := eventData.Create(ctx); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventCreateResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventCreateResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &EventCreateResponse{
+ EventId: eventData.EventId.String(),
+ },
+ }
+
+ return
+}
diff --git a/service/service_event/delete.go b/service/service_event/delete.go
new file mode 100644
index 0000000..4508131
--- /dev/null
+++ b/service/service_event/delete.go
@@ -0,0 +1,91 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type EventDeleteData struct {
+ EventId string `json:"event_id" validate:"required"`
+}
+
+type EventDeletePayload struct {
+ Context context.Context
+ Data *EventDeleteData
+}
+
+type EventDeleteResult struct {
+ Common shared.CommonResult
+}
+
+func (self *EventServiceImpl) Delete(payload *EventDeletePayload) (result *EventDeleteResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "delete",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventDelete)
+
+ eventId, err := uuid.Parse(payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventDeleteResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ if err := new(data.Event).DeleteEventById(ctx, eventId); err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventDeleteNotFound),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventDeleteResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventDeleteResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventDeleteResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+}
diff --git a/service/service_event/event_test.go b/service/service_event/event_test.go
new file mode 100644
index 0000000..bba4045
--- /dev/null
+++ b/service/service_event/event_test.go
@@ -0,0 +1,801 @@
+package service_event
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/testutil"
+)
+
+
+func newEventSvc() EventService { return NewEventService() }
+
+func makeOwner(t *testing.T, permLevel uint) *data.User {
+ t.Helper()
+ return testutil.SeedUser(t, uuid.New().String()+"@test.com", permLevel)
+}
+
+func strPtr(s string) *string { return &s }
+
+func createEventForOwner(t *testing.T, ctx context.Context, owner *data.User, typ string) *EventCreateResult {
+ t.Helper()
+ svc := newEventSvc()
+ return svc.Create(&EventCreatePayload{
+ Context: ctx,
+ Data: &EventCreateData{
+ UserId: owner.UserId.String(),
+ PermissionLevel: owner.PermissionLevel,
+ Type: typ,
+ Name: "Test Event " + uuid.New().String()[:8],
+ Subtitle: "subtitle",
+ StartTime: time.Now().Add(24 * time.Hour),
+ EndTime: time.Now().Add(48 * time.Hour),
+ Quota: 100,
+ Limit: 150,
+ },
+ })
+}
+
+// ---- Create ----
+
+func TestEventCreateSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ result := createEventForOwner(t, ctx, owner, "official")
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.NotEmpty(t, result.Data.EventId)
+}
+
+func TestEventCreateLv30OnlyParty(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 30)
+
+ partyResult := createEventForOwner(t, ctx, owner, "party")
+ assert.Equal(t, 200, partyResult.Common.HttpCode)
+
+ officialResult := createEventForOwner(t, ctx, owner, "official")
+ assert.Equal(t, 403, officialResult.Common.HttpCode)
+ assert.Equal(t, exception.EventCreateTypeNotAllowed, officialResult.Common.Exception.Original)
+}
+
+func TestEventCreateInvalidType(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ svc := newEventSvc()
+ result := svc.Create(&EventCreatePayload{
+ Context: ctx,
+ Data: &EventCreateData{
+ UserId: owner.UserId.String(),
+ PermissionLevel: owner.PermissionLevel,
+ Type: "invalid-type",
+ Name: "Event",
+ StartTime: time.Now().Add(time.Hour),
+ EndTime: time.Now().Add(2 * time.Hour),
+ Quota: 10,
+ Limit: 20,
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+// ---- List ----
+
+func TestEventList(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ for i := 0; i < 3; i++ {
+ r := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, r.Common.HttpCode)
+ }
+
+ limit := "10"
+ offset := "0"
+ svc := newEventSvc()
+ result := svc.List(&EventListPayload{
+ Context: ctx,
+ UserId: owner.UserId,
+ Data: &EventListData{
+ Limit: &limit,
+ Offset: &offset,
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.GreaterOrEqual(t, len(*result.Data.Items), 3)
+ assert.GreaterOrEqual(t, result.Data.Total, int64(3))
+}
+
+func TestEventListLv30ShowsOwnOnly(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ lv30Owner := makeOwner(t, 30)
+ otherOwner := makeOwner(t, 40)
+
+ r := createEventForOwner(t, ctx, lv30Owner, "party")
+ require.Equal(t, 200, r.Common.HttpCode)
+ r = createEventForOwner(t, ctx, otherOwner, "party")
+ require.Equal(t, 200, r.Common.HttpCode)
+
+ limit := "10"
+ offset := "0"
+ svc := newEventSvc()
+ result := svc.List(&EventListPayload{
+ Context: ctx,
+ UserId: lv30Owner.UserId,
+ Data: &EventListData{
+ Limit: &limit,
+ Offset: &offset,
+ PermissionLevel: 30,
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ for _, ev := range *result.Data.Items {
+ assert.Equal(t, lv30Owner.UserId.String(), ev.Owner)
+ }
+}
+
+func TestEventListRequiresOffset(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newEventSvc()
+ result := svc.List(&EventListPayload{
+ Context: context.Background(),
+ UserId: uuid.New(),
+ Data: &EventListData{},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+// ---- GetInfo ----
+
+func TestEventGetInfo(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "official")
+ require.Equal(t, 200, cr.Common.HttpCode)
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+
+ svc := newEventSvc()
+ result := svc.GetInfo(&EventInfoPayload{
+ Context: ctx,
+ UserId: owner.UserId,
+ Data: &EventInfoData{EventId: eventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, cr.Data.EventId, result.Data.EventId)
+}
+
+func TestEventGetInfoNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newEventSvc()
+ result := svc.GetInfo(&EventInfoPayload{
+ Context: context.Background(),
+ UserId: uuid.New(),
+ Data: &EventInfoData{EventId: uuid.New()},
+ })
+
+ assert.Equal(t, 404, result.Common.HttpCode)
+ assert.Equal(t, exception.EventInfoNotFound, result.Common.Exception.Original)
+}
+
+// ---- Join ----
+
+func TestEventJoinSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := testutil.SeedUser(t, uuid.New().String()+"@test.com", 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.NotEmpty(t, result.Data.AttendanceId)
+}
+
+func TestEventJoinKycRequired(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := testutil.SeedUser(t, uuid.New().String()+"@test.com", 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ svc := newEventSvc()
+ cr := svc.Create(&EventCreatePayload{
+ Context: ctx,
+ Data: &EventCreateData{
+ UserId: owner.UserId.String(),
+ PermissionLevel: owner.PermissionLevel,
+ Type: "party",
+ Name: "KYC Event",
+ StartTime: time.Now().Add(time.Hour),
+ EndTime: time.Now().Add(2 * time.Hour),
+ EnableKYC: true,
+ Quota: 100,
+ Limit: 150,
+ },
+ })
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ result := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ KycId: "",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestEventJoinNicknameRequired(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := testutil.SeedUser(t, uuid.New().String()+"@test.com", 10)
+ // No nickname set
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+// ---- Update ----
+
+func TestEventUpdateSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Update(&EventUpdatePayload{
+ Context: ctx,
+ Data: &EventUpdateData{
+ EventId: cr.Data.EventId,
+ UserId: owner.UserId.String(),
+ Name: strPtr("Updated Event Name"),
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestEventUpdateQuotaAndLimit(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ eventId, err := uuid.Parse(cr.Data.EventId)
+ require.NoError(t, err)
+
+ quota := int64(200)
+ limit := int64(250)
+ svc := newEventSvc()
+ result := svc.Update(&EventUpdatePayload{
+ Context: ctx,
+ Data: &EventUpdateData{
+ EventId: cr.Data.EventId,
+ UserId: owner.UserId.String(),
+ Quota: "a,
+ Limit: &limit,
+ },
+ })
+ require.Equal(t, 200, result.Common.HttpCode)
+
+ ev, err := new(data.Event).GetEventById(ctx, eventId)
+ require.NoError(t, err)
+ require.NotNil(t, ev)
+ assert.Equal(t, quota, ev.Quota)
+ assert.Equal(t, limit, ev.Limit)
+}
+
+func TestEventUpdateNotOwner(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 30)
+ notOwner := makeOwner(t, 30)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Update(&EventUpdatePayload{
+ Context: ctx,
+ Data: &EventUpdateData{
+ EventId: cr.Data.EventId,
+ UserId: notOwner.UserId.String(),
+ PermissionLevel: notOwner.PermissionLevel,
+ Name: strPtr("Hacked"),
+ },
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+ assert.Equal(t, exception.EventUpdateNotOwner, result.Common.Exception.Original)
+}
+
+// Lv40+ admins may bypass the owner-only restriction.
+func TestEventUpdateAdminBypass(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 30)
+ admin := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Update(&EventUpdatePayload{
+ Context: ctx,
+ Data: &EventUpdateData{
+ EventId: cr.Data.EventId,
+ UserId: admin.UserId.String(),
+ PermissionLevel: admin.PermissionLevel,
+ Name: strPtr("Edited By Admin"),
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestEventUpdateImmutableFields(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ typeVal := "official"
+ svc := newEventSvc()
+ result := svc.Update(&EventUpdatePayload{
+ Context: ctx,
+ Data: &EventUpdateData{
+ EventId: cr.Data.EventId,
+ UserId: owner.UserId.String(),
+ Type: &typeVal,
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+ assert.Equal(t, exception.EventUpdateImmutableField, result.Common.Exception.Original)
+}
+
+// ---- Delete ----
+
+func TestEventDeleteSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Delete(&EventDeletePayload{
+ Context: context.Background(),
+ Data: &EventDeleteData{EventId: cr.Data.EventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestEventDeleteNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newEventSvc()
+ result := svc.Delete(&EventDeletePayload{
+ Context: context.Background(),
+ Data: &EventDeleteData{EventId: uuid.New().String()},
+ })
+
+ assert.Equal(t, 404, result.Common.HttpCode)
+}
+
+// ---- Stats ----
+
+func TestEventStats(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+ owner := makeOwner(t, 40)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Stats(&EventStatsPayload{
+ Context: ctx,
+ UserId: owner.UserId,
+ Data: &EventStatsData{EventId: cr.Data.EventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, int64(0), result.Data.JoinCount)
+}
+
+func TestEventStatsNotOwner(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ notOwner := makeOwner(t, 30)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ result := svc.Stats(&EventStatsPayload{
+ Context: ctx,
+ UserId: notOwner.UserId,
+ Data: &EventStatsData{EventId: cr.Data.EventId},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+ assert.Equal(t, exception.EventStatsNotOwner, result.Common.Exception.Original)
+}
+
+// ---- Checkin ----
+
+func TestEventCheckinSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := makeOwner(t, 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ jr := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+ require.Equal(t, 200, jr.Common.HttpCode)
+
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+ result := svc.Checkin(&CheckinPayload{
+ Context: ctx,
+ UserId: joiner.UserId,
+ Data: &CheckinData{EventId: eventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ require.NotNil(t, result.Data.CheckinCode)
+ assert.Len(t, *result.Data.CheckinCode, 6)
+}
+
+func TestEventCheckinNotAttendee(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ notJoiner := makeOwner(t, 10)
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+ svc := newEventSvc()
+ result := svc.Checkin(&CheckinPayload{
+ Context: ctx,
+ UserId: notJoiner.UserId,
+ Data: &CheckinData{EventId: eventId},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+// ---- CheckinSubmit ----
+
+func TestEventCheckinSubmitSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := makeOwner(t, 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ jr := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+ require.Equal(t, 200, jr.Common.HttpCode)
+
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+ checkinResult := svc.Checkin(&CheckinPayload{
+ Context: ctx,
+ UserId: joiner.UserId,
+ Data: &CheckinData{EventId: eventId},
+ })
+ require.Equal(t, 200, checkinResult.Common.HttpCode)
+
+ submitResult := svc.CheckinSubmit(&CheckinSubmitPayload{
+ Context: ctx,
+ Data: &CheckinSubmitData{CheckinCode: *checkinResult.Data.CheckinCode},
+ })
+ assert.Equal(t, 200, submitResult.Common.HttpCode)
+}
+
+func TestEventCheckinSubmitInvalidCode(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newEventSvc()
+ result := svc.CheckinSubmit(&CheckinSubmitPayload{
+ Context: context.Background(),
+ Data: &CheckinSubmitData{CheckinCode: "000000"},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+// ---- CheckinQuery ----
+
+func TestEventCheckinQueryNotCheckedIn(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := makeOwner(t, 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ svc := newEventSvc()
+ jr := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+ require.Equal(t, 200, jr.Common.HttpCode)
+
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+ result := svc.CheckinQuery(&CheckinQueryPayload{
+ Context: ctx,
+ UserId: joiner.UserId,
+ Data: &CheckinQueryData{EventId: eventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Nil(t, result.Data.CheckinAt)
+}
+
+func TestEventCheckinQueryNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newEventSvc()
+ result := svc.CheckinQuery(&CheckinQueryPayload{
+ Context: context.Background(),
+ UserId: uuid.New(),
+ Data: &CheckinQueryData{EventId: uuid.New()},
+ })
+
+ assert.Equal(t, 404, result.Common.HttpCode)
+}
+
+// ---- AttendanceList ----
+
+func TestEventAttendanceListSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+
+ svc := newEventSvc()
+ for i := 0; i < 2; i++ {
+ joiner := makeOwner(t, 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+ jr := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+ require.Equal(t, 200, jr.Common.HttpCode)
+ }
+
+ limit := "10"
+ offset := "0"
+ result := svc.AttendanceList(&AttendanceListPayload{
+ Context: ctx,
+ UserId: owner.UserId,
+ Data: &AttendanceListData{
+ EventId: eventId,
+ Limit: &limit,
+ Offset: &offset,
+ },
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, int64(2), result.Data.Total)
+ assert.Len(t, result.Data.Items, 2)
+}
+
+func TestEventAttendanceListNotOwner(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ notOwner := makeOwner(t, 30)
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+
+ svc := newEventSvc()
+ result := svc.AttendanceList(&AttendanceListPayload{
+ Context: ctx,
+ UserId: notOwner.UserId,
+ Data: &AttendanceListData{EventId: eventId},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+func TestEventAttendanceListNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newEventSvc()
+ result := svc.AttendanceList(&AttendanceListPayload{
+ Context: context.Background(),
+ UserId: uuid.New(),
+ Data: &AttendanceListData{EventId: uuid.New()},
+ })
+
+ assert.Equal(t, 404, result.Common.HttpCode)
+}
+
+// ---- GetAttendanceGuide ----
+
+func TestEventGetAttendanceGuideSuccess(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ joiner := makeOwner(t, 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ cr := createEventForOwner(t, ctx, owner, "party")
+ require.Equal(t, 200, cr.Common.HttpCode)
+ eventId, _ := uuid.Parse(cr.Data.EventId)
+
+ require.NoError(t, new(data.Event).PatchByEventId(ctx, eventId, data.WithAttendanceGuide("base64guidetext")))
+
+ svc := newEventSvc()
+ jr := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+ require.Equal(t, 200, jr.Common.HttpCode)
+
+ result := svc.GetAttendanceGuide(&AttendanceGuidePayload{
+ Context: ctx,
+ UserId: joiner.UserId,
+ Data: &AttendanceGuideData{EventId: eventId},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, "base64guidetext", result.Data.AttendanceGuide)
+}
+
+// ---- Join edge cases ----
+
+func TestEventJoinLimitExceeded(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ owner := makeOwner(t, 40)
+ svc := newEventSvc()
+
+ cr := svc.Create(&EventCreatePayload{
+ Context: ctx,
+ Data: &EventCreateData{
+ UserId: owner.UserId.String(),
+ PermissionLevel: owner.PermissionLevel,
+ Type: "party",
+ Name: "Zero Capacity Event",
+ Subtitle: "sub",
+ StartTime: time.Now().Add(time.Hour),
+ EndTime: time.Now().Add(2 * time.Hour),
+ Quota: 0,
+ Limit: 0,
+ },
+ })
+ require.Equal(t, 200, cr.Common.HttpCode)
+
+ joiner := makeOwner(t, 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, joiner.UserId, data.WithNickname("Joiner")))
+
+ result := svc.Join(&EventJoinPayload{
+ Context: ctx,
+ Data: &EventJoinData{
+ EventId: cr.Data.EventId,
+ UserId: joiner.UserId.String(),
+ },
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+ assert.Equal(t, exception.EventJoinLimitExceeded, result.Common.Exception.Original)
+}
diff --git a/service/service_event/get_attendance_guide.go b/service/service_event/get_attendance_guide.go
new file mode 100644
index 0000000..8369c23
--- /dev/null
+++ b/service/service_event/get_attendance_guide.go
@@ -0,0 +1,161 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type AttendanceGuideData struct {
+ EventId uuid.UUID `json:"event_id" validate:"required"`
+}
+
+type AttendanceGuidePayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *AttendanceGuideData
+}
+
+type AttendanceGuideResponse struct {
+ AttendanceGuide string `json:"attendance_guide" validate:"required"`
+}
+
+type AttendanceGuideResult struct {
+ Common shared.CommonResult
+ Data *AttendanceGuideResponse
+}
+
+func (self *EventServiceImpl) GetAttendanceGuide(payload *AttendanceGuidePayload) (result *AttendanceGuideResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "get_attendance_guide",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventGetAttendanceGuide)
+
+ attendanceData, err := new(data.Attendance).GetAttendance(ctx, payload.UserId, payload.Data.EventId)
+
+ notAttendee := err == gorm.ErrRecordNotFound || attendanceData == nil
+ if err != nil && err != gorm.ErrRecordNotFound {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceGuideResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if notAttendee {
+ callerData, err := new(data.User).GetByUserId(ctx, &payload.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceGuideResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if callerData == nil || callerData.PermissionLevel < 40 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("user is not an attendee of this event")),
+ ).Throw(ctx)
+
+ result = &AttendanceGuideResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+ }
+
+ if !notAttendee && attendanceData.State == "suspend" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorPermissionDenied),
+ exception.WithError(errors.New("attendance suspended")),
+ ).Throw(ctx)
+
+ result = &AttendanceGuideResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ eventData, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &AttendanceGuideResult{
+ Common: shared.CommonResult{
+ HttpCode: 404,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ result = &AttendanceGuideResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ Data: &AttendanceGuideResponse{
+ AttendanceGuide: eventData.AttendanceGuide,
+ },
+ }
+
+ return
+}
diff --git a/service/service_event/get_info.go b/service/service_event/get_info.go
new file mode 100644
index 0000000..74c63b7
--- /dev/null
+++ b/service/service_event/get_info.go
@@ -0,0 +1,168 @@
+package service_event
+
+import (
+ "context"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type EventInfoData struct {
+ EventId uuid.UUID `json:"event_id" validate:"required"`
+}
+
+type EventInfoPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *EventInfoData
+}
+
+type EventInfoResponse struct {
+ data.EventIndexDoc
+ Limit int64 `json:"limit"`
+ Quota int64 `json:"quota"`
+ IsJoined bool `json:"is_joined"`
+ IsCheckedIn bool `json:"is_checked_in"`
+ JoinCount int64 `json:"join_count"`
+ CheckinCount int64 `json:"checkin_count"`
+}
+
+type EventInfoResult struct {
+ Common shared.CommonResult
+ Data *EventInfoResponse
+}
+
+func (self *EventServiceImpl) GetInfo(payload *EventInfoPayload) (result *EventInfoResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "get_info",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventGetEventInfo)
+
+ event, err := new(data.Event).GetEventById(ctx, payload.Data.EventId)
+ if err != nil || event == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventInfoNotFound),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 404,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ joinCount, err := new(data.Attendance).CountUsersByEventID(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ checkinCount, err := new(data.Attendance).CountCheckedInUsersByEventID(ctx, payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+
+ return
+ }
+
+ var isJoined bool
+ joinedInfo, err := new(data.Attendance).GetAttendance(ctx, payload.UserId, payload.Data.EventId)
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ isJoined = false
+ } else {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+ return
+ }
+ } else {
+ if joinedInfo != nil && joinedInfo.AttendanceId != uuid.Nil {
+ isJoined = true
+ } else {
+ isJoined = false
+ }
+ }
+
+ result = &EventInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx),
+ },
+ Data: &EventInfoResponse{
+ EventIndexDoc: data.EventIndexDoc{
+ EventId: event.EventId.String(),
+ Name: event.Name,
+ Type: event.Type,
+ Subtitle: event.Subtitle,
+ Description: event.Description,
+ StartTime: event.StartTime,
+ EndTime: event.EndTime,
+ Thumbnail: event.Thumbnail,
+ EnableKYC: event.EnableKYC,
+ IsAgendaPublished: event.IsAgendaPublished,
+ Owner: event.Owner.String(),
+ },
+ Limit: event.Limit,
+ Quota: event.Quota,
+ IsJoined: isJoined,
+ IsCheckedIn: isJoined && joinedInfo != nil && !joinedInfo.CheckinAt.IsZero(),
+ JoinCount: joinCount,
+ CheckinCount: checkinCount,
+ },
+ }
+
+ return
+}
diff --git a/service/service_event/join.go b/service/service_event/join.go
new file mode 100644
index 0000000..3114a1f
--- /dev/null
+++ b/service/service_event/join.go
@@ -0,0 +1,304 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+type EventJoinData struct {
+ EventId string `json:"event_id" validate:"required"`
+ KycId string `json:"kyc_id"`
+ UserId string `json:"user_id" swaggerignore:"true"`
+ Role string `json:"role" swaggerignore:"true"`
+ State string `json:"state" swaggerignore:"true"`
+}
+
+type EventJoinPayload struct {
+ Context context.Context
+ Data *EventJoinData
+}
+
+type EventJoinResponse struct {
+ AttendanceId string `json:"attendance_id" validate:"required"`
+}
+
+type EventJoinResult struct {
+ Common shared.CommonResult
+ Data *EventJoinResponse
+}
+
+func (self *EventServiceImpl) Join(payload *EventJoinPayload) (result *EventJoinResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "join",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventJoin)
+
+ var err error
+
+ eventId, err := uuid.Parse(payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ eventData, err := new(data.Event).GetEventById(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if eventData.EnableKYC == true && payload.Data.KycId == "" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("KYC is required for this event")),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ userId, err := uuid.Parse(payload.Data.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ userData, err := new(data.User).GetByUserId(ctx, &userId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if userData.Nickname == "" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("user nickname is empty")),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ attendenceSearch, err := new(data.Attendance).GetAttendance(ctx, eventId, userId)
+
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ return &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+ }
+
+ if err == nil && attendenceSearch != nil && attendenceSearch.AttendanceId != uuid.Nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventJoinEventInvalid),
+ exception.WithError(errors.New("user already joined this event")),
+ ).Throw(ctx)
+
+ return &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+ }
+
+ attendenceCount, err := new(data.Attendance).CountUsersByEventID(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ return &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+ }
+
+ attendanceOpts := []data.AttendanceOption{
+ data.WithUserId(userId),
+ data.WithEventId(eventId),
+ data.WithRole("normal"),
+ }
+
+ if attendenceCount >= eventData.Quota {
+ if attendenceCount >= eventData.Limit {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventJoinLimitExceeded),
+ exception.WithError(errors.New("event limit exceeded")),
+ ).Throw(ctx)
+
+ return &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+ }
+ attendanceOpts = append(attendanceOpts, data.WithState("out_of_limit"))
+ } else {
+ attendanceOpts = append(attendanceOpts, data.WithState("success"))
+ }
+
+ if payload.Data.KycId != "" {
+ kycUUID, err := uuid.Parse(payload.Data.KycId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ return &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ }
+ }
+ attendanceOpts = append(attendanceOpts, data.WithKycId(kycUUID))
+ }
+
+ attendanceId, err := data.NewAttendance(attendanceOpts...).Create(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ return &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventJoinResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &EventJoinResponse{
+ AttendanceId: attendanceId.String(),
+ },
+ }
+
+ return
+}
diff --git a/service/service_event/list.go b/service/service_event/list.go
new file mode 100644
index 0000000..08b15a1
--- /dev/null
+++ b/service/service_event/list.go
@@ -0,0 +1,243 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "strconv"
+
+ "github.com/google/uuid"
+)
+
+type EventListData struct {
+ Limit *string `json:"limit"`
+ Offset *string `json:"offset" validate:"required"`
+ Type *string `json:"type"`
+ SortBy *string `json:"sort_by"`
+ SortOrder *string `json:"sort_order"`
+ PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
+}
+
+type EventListPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *EventListData
+}
+
+type EventListItems struct {
+ data.EventIndexDoc
+ IsJoined bool `json:"is_joined"`
+ IsCheckedIn bool `json:"is_checked_in"`
+ JoinCount int64 `json:"join_count"`
+ CheckinCount int64 `json:"checkin_count"`
+}
+
+type EventListResponse struct {
+ Items *[]EventListItems `json:"items"`
+ Total int64 `json:"total"`
+}
+
+type EventListResult struct {
+ Common shared.CommonResult
+ Data *EventListResponse `json:"event_list"`
+}
+
+func (self *EventServiceImpl) List(payload *EventListPayload) (result *EventListResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventList)
+
+ var limit string
+ if payload.Data.Limit == nil || *payload.Data.Limit == "" {
+ limit = "20"
+ } else {
+ limit = *payload.Data.Limit
+ }
+
+ var offset string
+ if payload.Data.Offset == nil || *payload.Data.Offset == "" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("offset is required")),
+ ).Throw(ctx)
+
+ return &EventListResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ } else {
+ offset = *payload.Data.Offset
+ }
+
+ limitNum, err := strconv.Atoi(limit)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ offsetNum, err := strconv.Atoi(offset)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ opts := data.EventListOptions{
+ Limit: int64(limitNum),
+ Offset: int64(offsetNum),
+ }
+
+ if payload.Data.Type != nil && *payload.Data.Type != "" {
+ opts.TypeFilter = *payload.Data.Type
+ }
+
+ if payload.Data.SortBy != nil {
+ opts.SortBy = *payload.Data.SortBy
+ }
+
+ if payload.Data.SortOrder != nil {
+ opts.SortOrder = *payload.Data.SortOrder
+ }
+
+ // Lv30 users only see their own events
+ if payload.Data.PermissionLevel == 30 {
+ opts.OwnerId = &payload.UserId
+ }
+
+ eventList, total, err := new(data.Event).ListEventsWithOptions(ctx, opts)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ return &EventListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ }
+
+ items := make([]EventListItems, 0)
+
+ if eventList != nil && len(*eventList) > 0 {
+ var eventIds []uuid.UUID
+ for _, e := range *eventList {
+ if parsedId, parseErr := uuid.Parse(e.EventId); parseErr == nil {
+ eventIds = append(eventIds, parsedId)
+ }
+ }
+
+ joinedMap, err := new(data.Attendance).GetJoinedEventIDs(ctx, payload.UserId, eventIds)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ checkedInMap, err := new(data.Attendance).GetCheckedInEventIDs(ctx, payload.UserId, eventIds)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ joinCountMap, err := new(data.Attendance).CountUsersByEventIDs(ctx, eventIds)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ checkinCountMap, err := new(data.Attendance).CountCheckedInUsersByEventIDs(ctx, eventIds)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ for _, i := range *eventList {
+ currentIdUuid, _ := uuid.Parse(i.EventId)
+ items = append(items, EventListItems{
+ EventIndexDoc: i,
+ IsJoined: joinedMap[currentIdUuid],
+ IsCheckedIn: checkedInMap[currentIdUuid],
+ JoinCount: joinCountMap[currentIdUuid],
+ CheckinCount: checkinCountMap[currentIdUuid],
+ })
+ }
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventListResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: &EventListResponse{
+ Items: &items,
+ Total: total,
+ },
+ }
+ return
+}
diff --git a/service/service_event/service.go b/service/service_event/service.go
new file mode 100644
index 0000000..b2234cd
--- /dev/null
+++ b/service/service_event/service.go
@@ -0,0 +1,22 @@
+package service_event
+
+type EventService interface {
+ Checkin(*CheckinPayload) *CheckinResult
+ CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult
+ CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult
+ GetInfo(*EventInfoPayload) *EventInfoResult
+ List(*EventListPayload) *EventListResult
+ Join(*EventJoinPayload) *EventJoinResult
+ AttendanceList(*AttendanceListPayload) *AttendanceListResult
+ GetAttendanceGuide(*AttendanceGuidePayload) *AttendanceGuideResult
+ Create(*EventCreatePayload) *EventCreateResult
+ Update(*EventUpdatePayload) *EventUpdateResult
+ Delete(*EventDeletePayload) *EventDeleteResult
+ Stats(*EventStatsPayload) *EventStatsResult
+}
+
+type EventServiceImpl struct{}
+
+func NewEventService() EventService {
+ return &EventServiceImpl{}
+}
diff --git a/service/service_event/stats.go b/service/service_event/stats.go
new file mode 100644
index 0000000..e33f143
--- /dev/null
+++ b/service/service_event/stats.go
@@ -0,0 +1,204 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type EventStatsData struct {
+ EventId string `json:"event_id" validate:"required"`
+}
+
+type EventStatsPayload struct {
+ Context context.Context
+ UserId uuid.UUID
+ Data *EventStatsData
+}
+
+type EventStatsResponse struct {
+ JoinCount int64 `json:"join_count"`
+ CheckinCount int64 `json:"checkin_count"`
+ KycPassRate float64 `json:"kyc_pass_rate"`
+ AgendaSubmissionCount int64 `json:"agenda_submission_count"`
+}
+
+type EventStatsResult struct {
+ Common shared.CommonResult
+ Data *EventStatsResponse
+}
+
+func (self *EventServiceImpl) Stats(payload *EventStatsPayload) (result *EventStatsResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "stats",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventStats)
+
+ eventId, err := uuid.Parse(payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ eventData, err := new(data.Event).GetEventById(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ if eventData.Owner != payload.UserId {
+ callerData, err := new(data.User).GetByUserId(ctx, &payload.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if callerData == nil || callerData.PermissionLevel < 40 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventStatsNotOwner),
+ exception.WithError(errors.New("only the event owner may view event stats")),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+ }
+
+ attRepo := new(data.Attendance)
+
+ joinCount, err := attRepo.CountUsersByEventID(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ checkinCount, err := attRepo.CountCheckedInUsersByEventID(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ withKycCount, err := attRepo.CountWithKycByEventID(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ agendaCount, err := new(data.Agenda).CountByEventId(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ var kycPassRate float64
+ if joinCount > 0 {
+ kycPassRate = float64(withKycCount) / float64(joinCount) * 100
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventStatsResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: &EventStatsResponse{
+ JoinCount: joinCount,
+ CheckinCount: checkinCount,
+ KycPassRate: kycPassRate,
+ AgendaSubmissionCount: agendaCount,
+ },
+ }
+ return
+}
diff --git a/service/service_event/update.go b/service/service_event/update.go
new file mode 100644
index 0000000..09d9be8
--- /dev/null
+++ b/service/service_event/update.go
@@ -0,0 +1,212 @@
+package service_event
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type EventUpdateData struct {
+ EventId string `json:"event_id" validate:"required"`
+ Name *string `json:"name"`
+ Subtitle *string `json:"subtitle"`
+ Description *string `json:"description"`
+ AttendanceGuide *string `json:"attendance_guide"`
+ StartTime *time.Time `json:"start_time"`
+ EndTime *time.Time `json:"end_time"`
+ Thumbnail *string `json:"thumbnail"`
+ IsAgendaPublished *bool `json:"is_agenda_published"`
+ Quota *int64 `json:"quota"`
+ Limit *int64 `json:"limit"`
+ // immutable — presence triggers rejection
+ Type *string `json:"type" swaggerignore:"true"`
+ EnableKYC *bool `json:"enable_kyc" swaggerignore:"true"`
+ UserId string `json:"user_id" swaggerignore:"true"`
+ PermissionLevel uint `json:"permission_level" swaggerignore:"true"`
+}
+
+type EventUpdatePayload struct {
+ Context context.Context
+ Data *EventUpdateData
+}
+
+type EventUpdateResult struct {
+ Common shared.CommonResult
+}
+
+func (self *EventServiceImpl) Update(payload *EventUpdatePayload) (result *EventUpdateResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_event",
+ "update",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceEventUpdate)
+
+ if payload.Data.Type != nil || payload.Data.EnableKYC != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventUpdateImmutableField),
+ exception.WithError(errors.New("type and enable_kyc are immutable after creation")),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ eventId, err := uuid.Parse(payload.Data.EventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ userId, err := uuid.Parse(payload.Data.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ eventData, err := new(data.Event).GetEventById(ctx, eventId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if eventData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("event not found")),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ // Lv40+ users (admins) bypass the event-owner restriction.
+ if eventData.Owner != userId && payload.Data.PermissionLevel < 40 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.EventUpdateNotOwner),
+ exception.WithError(errors.New("only the event owner may update this event")),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+
+ var opts []data.EventOption
+
+ if payload.Data.Name != nil {
+ opts = append(opts, data.WithEventName(*payload.Data.Name))
+ }
+ if payload.Data.Subtitle != nil {
+ opts = append(opts, data.WithEventSubtitle(*payload.Data.Subtitle))
+ }
+ if payload.Data.Description != nil {
+ opts = append(opts, data.WithEventDescription(*payload.Data.Description))
+ }
+ if payload.Data.StartTime != nil {
+ opts = append(opts, data.WithEventStartTime(*payload.Data.StartTime))
+ }
+ if payload.Data.EndTime != nil {
+ opts = append(opts, data.WithEventEndTime(*payload.Data.EndTime))
+ }
+ if payload.Data.Thumbnail != nil {
+ opts = append(opts, data.WithThumbnail(*payload.Data.Thumbnail))
+ }
+ if payload.Data.AttendanceGuide != nil {
+ opts = append(opts, data.WithAttendanceGuide(*payload.Data.AttendanceGuide))
+ }
+ if payload.Data.Quota != nil {
+ opts = append(opts, data.WithQuota(*payload.Data.Quota))
+ }
+ if payload.Data.Limit != nil {
+ opts = append(opts, data.WithLimit(*payload.Data.Limit))
+ }
+
+ if payload.Data.IsAgendaPublished != nil {
+ opts = append(opts, data.WithIsAgendaPublished(*payload.Data.IsAgendaPublished))
+ }
+
+ if len(opts) == 0 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+ }
+
+ if err := new(data.Event).PatchByEventId(ctx, eventId, opts...); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &EventUpdateResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+}
diff --git a/service/service_kyc/kyc_test.go b/service/service_kyc/kyc_test.go
new file mode 100644
index 0000000..b8f77e5
--- /dev/null
+++ b/service/service_kyc/kyc_test.go
@@ -0,0 +1,188 @@
+package service_kyc
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/testutil"
+)
+
+func newKycSvc() KycService {
+ return NewKycService()
+}
+
+// ---- SessionKyc input-validation paths (no external API required) ----
+
+func TestSessionKycInvalidBase64(t *testing.T) {
+ testutil.Setup(t)
+
+ result := newKycSvc().SessionKyc(&KycSessionPayload{
+ Context: context.Background(),
+ Data: &KycSessionData{
+ Type: "cnrid",
+ Identity: "!!!not-base64!!!",
+ UserId: "00000000-0000-0000-0000-000000000001",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestSessionKycInvalidJSONBodyCnrid(t *testing.T) {
+ testutil.Setup(t)
+
+ // Valid base64 but the decoded content is not valid JSON for CNRidInfo
+ badJSON := base64.StdEncoding.EncodeToString([]byte("not-json"))
+
+ result := newKycSvc().SessionKyc(&KycSessionPayload{
+ Context: context.Background(),
+ Data: &KycSessionData{
+ Type: "cnrid",
+ Identity: badJSON,
+ UserId: "00000000-0000-0000-0000-000000000001",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestSessionKycInvalidJSONBodyPassport(t *testing.T) {
+ testutil.Setup(t)
+
+ badJSON := base64.StdEncoding.EncodeToString([]byte("{invalid"))
+
+ result := newKycSvc().SessionKyc(&KycSessionPayload{
+ Context: context.Background(),
+ Data: &KycSessionData{
+ Type: "passport",
+ Identity: badJSON,
+ UserId: "00000000-0000-0000-0000-000000000001",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestSessionKycInvalidType(t *testing.T) {
+ testutil.Setup(t)
+
+ validB64 := base64.StdEncoding.EncodeToString([]byte(`{}`))
+
+ result := newKycSvc().SessionKyc(&KycSessionPayload{
+ Context: context.Background(),
+ Data: &KycSessionData{
+ Type: "unknown",
+ Identity: validB64,
+ UserId: "00000000-0000-0000-0000-000000000001",
+ },
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+// ---- QueryKyc input-validation paths ----
+
+func TestQueryKycExternalAPIError(t *testing.T) {
+ testutil.Setup(t)
+
+ // kyc.passport_reader_endpoint is unset, so GetSessionState will fail.
+ svc := newKycSvc()
+ result := svc.QueryKyc(&KycQueryPayload{
+ Context: context.Background(),
+ Data: &KycQueryData{KycId: "00000000-0000-0000-0000-000000000001"},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestQueryKycUnknownSessionState(t *testing.T) {
+ testutil.Setup(t)
+
+ // Start a mock server that returns a state string matching no known branch.
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]string{"state": "UNKNOWN_STATE_XYZ"})
+ })
+ srv := httptest.NewServer(mux)
+ t.Cleanup(srv.Close)
+ viper.Set("kyc.passport_reader_endpoint", srv.URL)
+
+ svc := &KycServiceImpl{PassportReaderSessionId: 1}
+ result := svc.QueryKyc(&KycQueryPayload{
+ Context: context.Background(),
+ Data: &KycQueryData{KycId: "00000000-0000-0000-0000-000000000001"},
+ })
+
+ require.NotNil(t, result, "result must never be nil")
+ assert.Equal(t, 500, result.Common.HttpCode)
+}
+
+func TestQueryKycPendingStates(t *testing.T) {
+ pendingStates := []string{"CREATED", "INITIATED", "COMPLETED"}
+
+ for _, s := range pendingStates {
+ s := s
+ t.Run(s, func(t *testing.T) {
+ testutil.Setup(t)
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]string{"state": s})
+ })
+ srv := httptest.NewServer(mux)
+ t.Cleanup(srv.Close)
+ viper.Set("kyc.passport_reader_endpoint", srv.URL)
+
+ svc := &KycServiceImpl{PassportReaderSessionId: 1}
+ result := svc.QueryKyc(&KycQueryPayload{
+ Context: context.Background(),
+ Data: &KycQueryData{KycId: "00000000-0000-0000-0000-000000000001"},
+ })
+
+ require.NotNil(t, result)
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, "pending", result.Data.Status)
+ })
+ }
+}
+
+func TestQueryKycFailedStates(t *testing.T) {
+ failedStates := []string{"FAILED", "ABORTED", "REJECTED"}
+
+ for _, s := range failedStates {
+ s := s
+ t.Run(s, func(t *testing.T) {
+ testutil.Setup(t)
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/session.state", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]string{"state": s})
+ })
+ srv := httptest.NewServer(mux)
+ t.Cleanup(srv.Close)
+ viper.Set("kyc.passport_reader_endpoint", srv.URL)
+
+ svc := &KycServiceImpl{PassportReaderSessionId: 1}
+ result := svc.QueryKyc(&KycQueryPayload{
+ Context: context.Background(),
+ Data: &KycQueryData{KycId: "00000000-0000-0000-0000-000000000001"},
+ })
+
+ require.NotNil(t, result)
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, "failed", result.Data.Status)
+ })
+ }
+}
diff --git a/service/service_kyc/query.go b/service/service_kyc/query.go
new file mode 100644
index 0000000..2c266bc
--- /dev/null
+++ b/service/service_kyc/query.go
@@ -0,0 +1,247 @@
+package service_kyc
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/internal/kyc"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type KycQueryData struct {
+ KycId string `json:"kyc_id" validate:"required"`
+}
+
+type KycQueryPayload struct {
+ Context context.Context
+ Data *KycQueryData
+}
+
+type KycQueryResponse struct {
+ Status string `json:"status" validate:"required"` // success | pending | failed
+}
+
+type KycQueryResult struct {
+ Common shared.CommonResult
+ Data *KycQueryResponse
+}
+
+func (self *KycServiceImpl) QueryKyc(payload *KycQueryPayload) (result *KycQueryResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_kyc",
+ "query",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceKycQuery)
+
+ var err error
+
+ sessionState, err := kyc.GetSessionState(ctx, self.PassportReaderSessionId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if sessionState == kyc.StateApproved {
+ sessionDetails, err := kyc.GetSessionDetails(ctx, self.PassportReaderSessionId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ var kycInfo = kyc.PassportResp{
+ GivenNames: sessionDetails.GivenNames,
+ Surname: sessionDetails.Surname,
+ Nationality: sessionDetails.Nationality,
+ DateOfBirth: sessionDetails.DateOfBirth,
+ DocumentType: sessionDetails.DocumentType,
+ DocumentNumber: sessionDetails.DocumentNumber,
+ ExpiryDate: sessionDetails.ExpiryDate,
+ }
+
+ if kycInfo.DocumentType != "PASSPORT" || kycInfo.DocumentNumber != self.PassportId {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ encodedKycInfo, err := kyc.EncodeAES(kycInfo)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorAesEncodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+ return
+ }
+
+ kycId, err := uuid.Parse(payload.Data.KycId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ err = new(data.Kyc).
+ PatchByKycId(ctx, &kycId, data.WithKycInfo(*encodedKycInfo))
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &KycQueryResponse{
+ Status: "success",
+ },
+ }
+
+ return
+ }
+
+ if sessionState == kyc.StateCreated || sessionState == kyc.StateInitiated || sessionState == kyc.StateCompleted {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &KycQueryResponse{
+ Status: "pending",
+ },
+ }
+
+ return
+ }
+
+ if sessionState == kyc.StateFailed || sessionState == kyc.StateAborted || sessionState == kyc.StateRejected {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &KycQueryResponse{
+ Status: "failed",
+ },
+ }
+
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("unknown session state: "+string(sessionState))),
+ ).Throw(ctx)
+
+ result = &KycQueryResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ }
+ return
+}
diff --git a/service/service_kyc/service.go b/service/service_kyc/service.go
new file mode 100644
index 0000000..4770111
--- /dev/null
+++ b/service/service_kyc/service.go
@@ -0,0 +1,15 @@
+package service_kyc
+
+type KycService interface {
+ SessionKyc(*KycSessionPayload) *KycSessionResult
+ QueryKyc(*KycQueryPayload) *KycQueryResult
+}
+
+type KycServiceImpl struct {
+ PassportId string `json:"passport_id"`
+ PassportReaderSessionId int `json:"passport_reader_session_id"`
+}
+
+func NewKycService() KycService {
+ return &KycServiceImpl{}
+}
diff --git a/service/service_kyc/session.go b/service/service_kyc/session.go
new file mode 100644
index 0000000..5a915da
--- /dev/null
+++ b/service/service_kyc/session.go
@@ -0,0 +1,379 @@
+package service_kyc
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "net/url"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/internal/kyc"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+// cnrid: {"legal_name":"", "resident_id":""}
+// passport: {"id": ""}
+type KycSessionData struct {
+ Type string `json:"type" validate:"required"` // cnrid | passport
+ Identity string `json:"identity" validate:"required"` // base64 json
+ UserId string `json:"user_id" swaggerignore:"true"`
+}
+
+type KycSessionPayload struct {
+ Context context.Context
+ Data *KycSessionData
+}
+
+type KycSessionResponse struct {
+ Status string `json:"status" validate:"required"` // success | processing
+ KycId *string `json:"kyc_id"`
+ RedirectUri *string `json:"redirect_uri"`
+}
+
+type KycSessionResult struct {
+ Common shared.CommonResult
+ Data *KycSessionResponse
+}
+
+func (self *KycServiceImpl) SessionKyc(payload *KycSessionPayload) (result *KycSessionResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_kyc",
+ "session",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceKycSession)
+
+ var err error
+
+ decodedIdentityByte, err := base64.StdEncoding.DecodeString(payload.Data.Identity)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorBase64DecodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ switch payload.Data.Type {
+ case "cnrid":
+ var info kyc.CNRidInfo
+ if err := json.Unmarshal(decodedIdentityByte, &info); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorJsonDecodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ kycInfo := kyc.CNRidInfo{
+ LegalName: info.LegalName,
+ ResidentId: info.ResidentId,
+ }
+
+ aliCloudAuth, err := kyc.CNRidMD5AliEnc(&kycInfo)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ kycResult, err := kyc.AliId2MetaVerify(aliCloudAuth)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if *kycResult != "1" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.KycSessionFailed),
+ exception.WithError(errors.New("KYC verification failed")),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ userId, err := uuid.Parse(payload.Data.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ encodedKycInfo, err := kyc.EncodeAES(kycInfo)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorAesEncodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ kycIdOrig, err := data.NewKyc(
+ data.WithKycType("cnrid"),
+ data.WithKycUserId(userId),
+ data.WithKycInfo(*encodedKycInfo),
+ ).Create(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ kycId := kycIdOrig.String()
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &KycSessionResponse{
+ Status: "success",
+ KycId: &kycId,
+ RedirectUri: nil,
+ },
+ }
+
+ case "passport":
+ var info kyc.PassportInfo
+ if err := json.Unmarshal(decodedIdentityByte, &info); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorJsonDecodeFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ self.PassportId = info.ID
+
+ sessionResponse, err := kyc.CreateSession(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInternal),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ self.PassportReaderSessionId = sessionResponse.ID
+
+ userId, err := uuid.Parse(payload.Data.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUuidParseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ kycIdOrig, err := data.NewKyc(
+ data.WithKycType("passport"),
+ data.WithKycUserId(userId),
+ ).Create(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 500,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ kycId := kycIdOrig.String()
+ kycBaseURL := "https://passportreader.app/open"
+ kycUrl, _ := url.Parse(kycBaseURL)
+ kycQuery := kycUrl.Query()
+ kycQuery.Set("token", sessionResponse.Token)
+ kycUrl.RawQuery = kycQuery.Encode()
+
+ redirectUri := kycUrl.String()
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ Data: &KycSessionResponse{
+ Status: "processing",
+ KycId: &kycId,
+ RedirectUri: &redirectUri,
+ },
+ }
+
+ default:
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &KycSessionResult{
+ Common: shared.CommonResult{
+ HttpCode: 400,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ return
+}
diff --git a/service/service_stats/global.go b/service/service_stats/global.go
new file mode 100644
index 0000000..e131518
--- /dev/null
+++ b/service/service_stats/global.go
@@ -0,0 +1,98 @@
+package service_stats
+
+import (
+ "context"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+)
+
+type GlobalStatsPayload struct {
+ Context context.Context
+}
+
+type GlobalStatsResponse struct {
+ TotalUsers int64 `json:"total_users"`
+ UsersPerLevel *[]data.PermissionLevelCount `json:"users_per_level"`
+ EventJoinCheckin *[]data.EventStatDoc `json:"event_join_checkin"`
+}
+
+type GlobalStatsResult struct {
+ Common shared.CommonResult
+ Data *GlobalStatsResponse
+}
+
+func (self *StatsServiceImpl) Global(payload *GlobalStatsPayload) (result *GlobalStatsResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_stats",
+ "global",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceStatsGlobal)
+
+ statsRepo := new(data.GlobalStats)
+
+ totalUsers, err := statsRepo.TotalUsers(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &GlobalStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ usersPerLevel, err := statsRepo.UsersPerPermissionLevel(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &GlobalStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ eventStats, err := statsRepo.EventJoinCheckinCounts(ctx)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &GlobalStatsResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &GlobalStatsResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Data: &GlobalStatsResponse{
+ TotalUsers: totalUsers,
+ UsersPerLevel: usersPerLevel,
+ EventJoinCheckin: eventStats,
+ },
+ }
+ return
+}
diff --git a/service/service_stats/service.go b/service/service_stats/service.go
new file mode 100644
index 0000000..2163373
--- /dev/null
+++ b/service/service_stats/service.go
@@ -0,0 +1,11 @@
+package service_stats
+
+type StatsService interface {
+ Global(*GlobalStatsPayload) *GlobalStatsResult
+}
+
+type StatsServiceImpl struct{}
+
+func NewStatsService() StatsService {
+ return &StatsServiceImpl{}
+}
diff --git a/service/service_stats/stats_test.go b/service/service_stats/stats_test.go
new file mode 100644
index 0000000..e4193b6
--- /dev/null
+++ b/service/service_stats/stats_test.go
@@ -0,0 +1,64 @@
+package service_stats
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/testutil"
+)
+
+func TestGlobalStatsEmpty(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := NewStatsService()
+ result := svc.Global(&GlobalStatsPayload{
+ Context: context.Background(),
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, int64(0), result.Data.TotalUsers)
+ assert.NotNil(t, result.Data.UsersPerLevel)
+ assert.Empty(t, *result.Data.UsersPerLevel)
+}
+
+func TestGlobalStatsWithData(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for _, lvl := range []uint{10, 10, 30} {
+ testutil.SeedUser(t, testutil.RandomEmail(), lvl)
+ }
+
+ svc := NewStatsService()
+ result := svc.Global(&GlobalStatsPayload{
+ Context: ctx,
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, int64(3), result.Data.TotalUsers)
+ require.NotNil(t, result.Data.UsersPerLevel)
+
+ countMap := make(map[uint]int64)
+ for _, r := range *result.Data.UsersPerLevel {
+ countMap[r.PermissionLevel] = r.Count
+ }
+ assert.Equal(t, int64(2), countMap[10])
+ assert.Equal(t, int64(1), countMap[30])
+}
+
+func TestGlobalStatsEventJoinCheckinField(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ svc := NewStatsService()
+ result := svc.Global(&GlobalStatsPayload{Context: ctx})
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ require.NotNil(t, result.Data.EventJoinCheckin, "EventJoinCheckin field must not be nil")
+ assert.Empty(t, *result.Data.EventJoinCheckin)
+}
diff --git a/service/service_user/get_info.go b/service/service_user/get_info.go
new file mode 100644
index 0000000..5ca4efe
--- /dev/null
+++ b/service/service_user/get_info.go
@@ -0,0 +1,122 @@
+package service_user
+
+import (
+ "context"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+
+ "github.com/google/uuid"
+)
+
+type UserInfoData struct {
+ UserId uuid.UUID `json:"user_id" validate:"required"`
+ Email string `json:"email" validate:"required"`
+ Username *string `json:"username" validate:"required"`
+ Nickname *string `json:"nickname"`
+ Subtitle *string `json:"subtitle"`
+ Avatar *string `json:"avatar"`
+ Bio *string `json:"bio"`
+ PermissionLevel *uint `json:"permission_level" validate:"required"`
+ AllowPublic *bool `json:"allow_public" validate:"required"`
+}
+
+type UserInfoPayload struct {
+ Context context.Context
+ UserId uuid.UUID // target user
+ OperatorId uuid.UUID // calling user (for permission matrix on update)
+ OperatorLevel uint
+ IsOther bool
+ Data *UserInfoData
+}
+
+type UserInfoResult struct {
+ Common shared.CommonResult
+ Data *UserInfoData
+}
+
+// GetUserInfo
+func (self *UserServiceImpl) GetInfo(payload *UserInfoPayload) (result *UserInfoResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_user",
+ "get_info",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceUserGetInfo)
+
+ var err error
+
+ userData, err := new(data.User).
+ GetByUserId(
+ ctx,
+ &payload.UserId,
+ )
+
+ if err != nil || userData == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUserNotFound),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 404,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+
+ if payload.IsOther {
+ if !userData.AllowPublic {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUserNotPublic),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 403,
+ Exception: exc,
+ },
+ Data: nil,
+ }
+
+ return
+ }
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{
+ HttpCode: 200,
+ Exception: exc,
+ },
+ 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.go b/service/service_user/list.go
new file mode 100644
index 0000000..e76526d
--- /dev/null
+++ b/service/service_user/list.go
@@ -0,0 +1,143 @@
+package service_user
+
+import (
+ "context"
+ "errors"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "strconv"
+)
+
+type UserListData struct {
+ Limit *string
+ Offset *string `validate:"required"`
+ SortBy *string
+ SortOrder *string
+ PermissionLevel *uint
+}
+
+type UserListPayload struct {
+ Context context.Context
+ Data *UserListData
+}
+
+type UserListResponse struct {
+ data.UserAdminDoc
+}
+
+type UserListResult struct {
+ Common shared.CommonResult
+ Total int64
+ Data *[]UserListResponse `json:"user_list"`
+}
+
+func (self *UserServiceImpl) List(payload *UserListPayload) (result *UserListResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_user",
+ "list",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceUserList)
+
+ limit := 20
+ if payload.Data.Limit != nil && *payload.Data.Limit != "" {
+ v, err := strconv.Atoi(*payload.Data.Limit)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserListResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ limit = v
+ }
+
+ var offset string
+ if payload.Data.Offset == nil || *payload.Data.Offset == "" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("offset is required")),
+ ).Throw(ctx)
+
+ result = &UserListResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ } else {
+ offset = *payload.Data.Offset
+ }
+
+ // Parse string to int64
+ offsetNum, err := strconv.Atoi(offset)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserListResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+
+ opts := data.UserListOptions{
+ Limit: limit,
+ Offset: offsetNum,
+ PermissionLevel: payload.Data.PermissionLevel,
+ }
+
+ if payload.Data.SortBy != nil {
+ opts.SortBy = *payload.Data.SortBy
+ }
+ if payload.Data.SortOrder != nil {
+ opts.SortOrder = *payload.Data.SortOrder
+ }
+
+ userList, total, err := new(data.User).ListUsersFiltered(ctx, opts)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.UserListDatabaseFailed),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserListResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ response := make([]UserListResponse, 0, len(*userList))
+ for _, doc := range *userList {
+ response = append(response, UserListResponse{UserAdminDoc: doc})
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &UserListResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ Total: total,
+ Data: &response,
+ }
+ return
+}
diff --git a/service/service_user/service.go b/service/service_user/service.go
new file mode 100644
index 0000000..feb3a3a
--- /dev/null
+++ b/service/service_user/service.go
@@ -0,0 +1,13 @@
+package service_user
+
+type UserService interface {
+ GetInfo(*UserInfoPayload) *UserInfoResult
+ UpdateInfo(*UserInfoPayload) *UserInfoResult
+ List(*UserListPayload) *UserListResult
+}
+
+type UserServiceImpl struct{}
+
+func NewUserService() UserService {
+ return &UserServiceImpl{}
+}
diff --git a/service/service_user/update_info.go b/service/service_user/update_info.go
new file mode 100644
index 0000000..8d92bbd
--- /dev/null
+++ b/service/service_user/update_info.go
@@ -0,0 +1,260 @@
+package service_user
+
+import (
+ "errors"
+ "net/url"
+ "nixcn-cms/data"
+ "nixcn-cms/internal/cryptography"
+ "nixcn-cms/internal/exception"
+ "nixcn-cms/service/shared"
+ "nixcn-cms/tracer"
+ "unicode/utf8"
+)
+
+type UserInfoUpdateData struct {
+ TargetUserId string `json:"user_id"`
+ 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"`
+} // Dummy struct for swagger
+
+func (self *UserServiceImpl) UpdateInfo(payload *UserInfoPayload) (result *UserInfoResult) {
+ ctx, span := tracer.StartSpan(
+ payload.Context,
+ "service_user",
+ "update_info",
+ )
+ defer span.End()
+
+ ctx = exception.ContextWithService(ctx, exception.ServiceUserUpdateInfo)
+
+ isAdminEdit := payload.OperatorLevel >= 40 && payload.UserId != payload.OperatorId
+
+ var opts []data.UserOption
+
+ if payload.Data.Username != nil {
+ val := *payload.Data.Username
+ if val != "" {
+ if len(val) < 5 || len(val) >= 255 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Username length must be between 5 and 255")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ }
+ opts = append(opts, data.WithUsername(val))
+ }
+
+ if payload.Data.Nickname != nil {
+ val := *payload.Data.Nickname
+ if val == "" {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Nickname cannot be empty")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ if utf8.RuneCountInString(val) > 24 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Nickname too long")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ opts = append(opts, data.WithNickname(val))
+ }
+
+ if payload.Data.Subtitle != nil {
+ val := *payload.Data.Subtitle
+ if utf8.RuneCountInString(val) > 32 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Subtitle too long")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ opts = append(opts, data.WithSubtitle(val))
+ }
+
+ if payload.Data.Avatar != nil {
+ val := *payload.Data.Avatar
+ if val != "" {
+ _, err := url.ParseRequestURI(val)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ }
+ opts = append(opts, data.WithAvatar(val))
+ }
+
+ if payload.Data.Bio != nil {
+ val := *payload.Data.Bio
+ if val != "" {
+ if !cryptography.IsBase64Std(val) {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorInvalidInput),
+ exception.WithError(errors.New("Bio must be base64 encoded")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 400, Exception: exc},
+ }
+ return
+ }
+ }
+ opts = append(opts, data.WithBio(val))
+ }
+
+ if payload.Data.AllowPublic != nil {
+ opts = append(opts, data.WithAllowPublic(*payload.Data.AllowPublic))
+ }
+
+ userData := new(data.User)
+
+ targetInfo, err := userData.GetByUserId(ctx, &payload.UserId)
+ if err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ if targetInfo == nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorUserNotFound),
+ exception.WithError(errors.New("target user not found")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 404, Exception: exc},
+ }
+ return
+ }
+
+ // Permission matrix: admin editing another user
+ if isAdminEdit {
+ if targetInfo.PermissionLevel >= payload.OperatorLevel {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.UserUpdatePermissionMatrixViolated),
+ exception.WithError(errors.New("cannot edit a user with equal or higher permission level")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+ }
+
+ if payload.Data.PermissionLevel != nil {
+ newLevel := *payload.Data.PermissionLevel
+ // Only admins may change permission_level; the new level must be strictly below the operator's
+ if payload.OperatorLevel >= 40 {
+ if newLevel >= payload.OperatorLevel {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusUser),
+ exception.WithType(exception.TypeSpecific),
+ exception.WithOriginal(exception.UserUpdatePermissionLevelTooHigh),
+ exception.WithError(errors.New("cannot grant a permission level equal to or higher than your own")),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 403, Exception: exc},
+ }
+ return
+ }
+ opts = append(opts, data.WithPermissionLevel(newLevel))
+ }
+ // Non-admins silently ignore permission_level changes
+ }
+
+ if len(opts) == 0 {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+ }
+
+ if err := userData.PatchByUserId(ctx, payload.UserId, opts...); err != nil {
+ exc := exception.New(
+ exception.WithStatus(exception.StatusServer),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonErrorDatabase),
+ exception.WithError(err),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 500, Exception: exc},
+ }
+ return
+ }
+
+ exc := exception.New(
+ exception.WithStatus(exception.StatusSuccess),
+ exception.WithType(exception.TypeCommon),
+ exception.WithOriginal(exception.CommonSuccess),
+ ).Throw(ctx)
+
+ result = &UserInfoResult{
+ Common: shared.CommonResult{HttpCode: 200, Exception: exc},
+ }
+ return
+}
diff --git a/service/service_user/user_test.go b/service/service_user/user_test.go
new file mode 100644
index 0000000..d7d1751
--- /dev/null
+++ b/service/service_user/user_test.go
@@ -0,0 +1,272 @@
+package service_user
+
+import (
+ "context"
+ "encoding/base64"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "nixcn-cms/data"
+ "nixcn-cms/testutil"
+)
+
+func newUserSvc() UserService { return NewUserService() }
+
+func strPtr(s string) *string { return &s }
+func uintPtr(u uint) *uint { return &u }
+
+// ---- GetInfo ----
+
+func TestUserGetInfoSelf(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "getinfo@example.com", 10)
+
+ svc := newUserSvc()
+ result := svc.GetInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: u.UserId,
+ OperatorId: u.UserId,
+ IsOther: false,
+ Data: &UserInfoData{},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+ require.NotNil(t, result.Data)
+ assert.Equal(t, u.Email, result.Data.Email)
+}
+
+func TestUserGetInfoOtherPublic(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ target := testutil.SeedUser(t, "public@example.com", 10)
+ require.NoError(t, new(data.User).PatchByUserId(ctx, target.UserId, data.WithAllowPublic(true)))
+
+ svc := newUserSvc()
+ result := svc.GetInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: target.UserId,
+ OperatorId: uuid.New(),
+ IsOther: true,
+ Data: &UserInfoData{},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestUserGetInfoOtherPrivate(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ target := testutil.SeedUser(t, "private@example.com", 10)
+ // AllowPublic defaults to false – no patch needed
+
+ svc := newUserSvc()
+ result := svc.GetInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: target.UserId,
+ OperatorId: uuid.New(),
+ IsOther: true,
+ Data: &UserInfoData{},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+func TestUserGetInfoNotFound(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newUserSvc()
+ result := svc.GetInfo(&UserInfoPayload{
+ Context: context.Background(),
+ UserId: uuid.New(),
+ Data: &UserInfoData{},
+ })
+
+ assert.Equal(t, 404, result.Common.HttpCode)
+}
+
+// ---- UpdateInfo ----
+
+func TestUserUpdateInfoNickname(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "update@example.com", 10)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: u.UserId,
+ OperatorId: u.UserId,
+ Data: &UserInfoData{Nickname: strPtr("New Nick")},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoEmptyNickname(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "update2@example.com", 10)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: u.UserId,
+ OperatorId: u.UserId,
+ Data: &UserInfoData{Nickname: strPtr("")},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoUsernameTooShort(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "update3@example.com", 10)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: u.UserId,
+ OperatorId: u.UserId,
+ Data: &UserInfoData{Username: strPtr("abc")},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoBioMustBeBase64(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "update4@example.com", 10)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: u.UserId,
+ OperatorId: u.UserId,
+ Data: &UserInfoData{Bio: strPtr("not-base64!!!")},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoBioBase64Valid(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ u := testutil.SeedUser(t, "update5@example.com", 10)
+ bio := base64.StdEncoding.EncodeToString([]byte("my bio"))
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: u.UserId,
+ OperatorId: u.UserId,
+ Data: &UserInfoData{Bio: strPtr(bio)},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoAdminPermissionMatrix(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ admin := testutil.SeedUser(t, "admin@example.com", 40)
+ target := testutil.SeedUser(t, "target@example.com", 30)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: target.UserId,
+ OperatorId: admin.UserId,
+ OperatorLevel: admin.PermissionLevel,
+ Data: &UserInfoData{Nickname: strPtr("Admin Edited")},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoAdminCannotEditPeer(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ admin1 := testutil.SeedUser(t, "admin1@example.com", 40)
+ admin2 := testutil.SeedUser(t, "admin2@example.com", 40)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: admin2.UserId,
+ OperatorId: admin1.UserId,
+ OperatorLevel: admin1.PermissionLevel,
+ Data: &UserInfoData{Nickname: strPtr("Hacked")},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+func TestUserUpdateInfoAdminCannotGrantSameLevel(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ admin := testutil.SeedUser(t, "adminlvl@example.com", 40)
+ target := testutil.SeedUser(t, "targetlvl@example.com", 10)
+ newLevel := uint(40)
+
+ svc := newUserSvc()
+ result := svc.UpdateInfo(&UserInfoPayload{
+ Context: ctx,
+ UserId: target.UserId,
+ OperatorId: admin.UserId,
+ OperatorLevel: admin.PermissionLevel,
+ Data: &UserInfoData{PermissionLevel: &newLevel},
+ })
+
+ assert.Equal(t, 403, result.Common.HttpCode)
+}
+
+// ---- List ----
+
+func TestUserListRequiresOffset(t *testing.T) {
+ testutil.Setup(t)
+
+ svc := newUserSvc()
+ result := svc.List(&UserListPayload{
+ Context: context.Background(),
+ Data: &UserListData{},
+ })
+
+ assert.Equal(t, 400, result.Common.HttpCode)
+}
+
+// The list service has a known bug where the offset variable is never
+// assigned from payload.Offset, causing strconv.Atoi to fail on "".
+// This test documents the actual (buggy) behaviour so we can track it.
+func TestUserListWithOffset(t *testing.T) {
+ testutil.Setup(t)
+ ctx := context.Background()
+
+ for i := 0; i < 3; i++ {
+ testutil.SeedUser(t, uuid.New().String()+"@example.com", 10)
+ }
+
+ offset := "0"
+ svc := newUserSvc()
+ result := svc.List(&UserListPayload{
+ Context: ctx,
+ Data: &UserListData{Offset: &offset},
+ })
+
+ assert.Equal(t, 200, result.Common.HttpCode)
+}
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() {}
diff --git a/testutil/testutil.go b/testutil/testutil.go
new file mode 100644
index 0000000..75e362b
--- /dev/null
+++ b/testutil/testutil.go
@@ -0,0 +1,172 @@
+package testutil
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "nixcn-cms/data"
+ "nixcn-cms/internal/cryptography"
+
+ "github.com/alicebob/miniredis/v2"
+ "github.com/glebarez/sqlite"
+ "github.com/google/uuid"
+ "github.com/redis/go-redis/v9"
+ "github.com/spf13/viper"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+// TestAESKey is a 32-byte AES key used for client_secret_key in tests.
+const TestAESKey = "testkey1234567890123456789012345"
+
+// TestKYCAESKey is a 16-byte AES key used for kyc_info_key in tests.
+const TestKYCAESKey = "testkyckey123456"
+
+// TestClientID is the OAuth client ID used across tests.
+const TestClientID = "test-client"
+
+// TestClientSecret is the plaintext client secret used in tests.
+const TestClientSecret = "testsecret-32bytes-padded-here!!"
+
+// SetupTestDB initialises an in-memory SQLite database and assigns it to
+// data.Database. It auto-migrates all models including Agenda.
+func SetupTestDB(t *testing.T) {
+ t.Helper()
+ db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{
+ Logger: logger.Default.LogMode(logger.Silent),
+ })
+ if err != nil {
+ t.Fatalf("SetupTestDB: open sqlite: %v", err)
+ }
+
+ if err := db.AutoMigrate(
+ &data.User{},
+ &data.Event{},
+ &data.Attendance{},
+ &data.Client{},
+ &data.Kyc{},
+ &data.Agenda{},
+ ); err != nil {
+ t.Fatalf("SetupTestDB: auto-migrate: %v", err)
+ }
+
+ data.Database = db
+
+ t.Cleanup(func() {
+ sqlDB, _ := db.DB()
+ _ = sqlDB.Close()
+ data.Database = nil
+ })
+}
+
+// SetupTestRedis starts an in-memory miniredis server and assigns it to
+// data.Redis. It returns the miniredis instance for time manipulation.
+func SetupTestRedis(t *testing.T) *miniredis.Miniredis {
+ t.Helper()
+ mr, err := miniredis.Run()
+ if err != nil {
+ t.Fatalf("SetupTestRedis: start miniredis: %v", err)
+ }
+
+ rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
+ data.Redis = rdb
+
+ t.Cleanup(func() {
+ _ = rdb.Close()
+ mr.Close()
+ data.Redis = nil
+ })
+
+ return mr
+}
+
+// SetupViper configures the minimum viper keys needed by internal packages
+// and services during tests.
+func SetupViper(t *testing.T) {
+ t.Helper()
+ viper.Reset()
+ viper.Set("server.application", "test-app")
+ viper.Set("server.debug_mode", true)
+ viper.Set("server.external_url", "http://localhost:8080")
+ viper.Set("secrets.client_secret_key", TestAESKey)
+ viper.Set("secrets.kyc_info_key", TestKYCAESKey)
+ viper.Set("secrets.turnstile_secret", "test-turnstile-secret")
+ viper.Set("ttl.auth_code_ttl", 10*time.Minute)
+ viper.Set("ttl.access_ttl", 15*time.Minute)
+ viper.Set("ttl.refresh_ttl", 7*24*time.Hour)
+ viper.Set("ttl.checkin_code_ttl", 5*time.Minute)
+ viper.Set("kyc.ali_access_key_id", "test-key-id")
+ viper.Set("kyc.ali_access_key_secret", "test-key-secret")
+ viper.Set("kyc.passport_reader_public_key", "test-pub")
+ viper.Set("kyc.passport_reader_secret", "test-secret")
+
+ t.Cleanup(func() { viper.Reset() })
+}
+
+// Setup is a convenience that calls SetupViper, SetupTestDB, and
+// SetupTestRedis, returning the miniredis instance.
+func Setup(t *testing.T) *miniredis.Miniredis {
+ t.Helper()
+ SetupViper(t)
+ SetupTestDB(t)
+ return SetupTestRedis(t)
+}
+
+// SeedClient creates a test OAuth client in the database and returns it.
+func SeedClient(t *testing.T) *data.Client {
+ t.Helper()
+ ctx := t.Context()
+
+ enc, err := cryptography.AESCBCEncrypt([]byte(TestClientSecret), []byte(TestAESKey))
+ if err != nil {
+ t.Fatalf("SeedClient: encrypt secret: %v", err)
+ }
+
+ redirectURIs := []string{"http://localhost/callback"}
+ urisJSON, _ := json.Marshal(redirectURIs)
+
+ client := &data.Client{
+ UUID: uuid.New(),
+ ClientId: TestClientID,
+ ClientSecret: enc,
+ ClientName: "Test Client",
+ RedirectUri: urisJSON,
+ }
+
+ if err := data.Database.WithContext(ctx).Create(client).Error; err != nil {
+ t.Fatalf("SeedClient: create: %v", err)
+ }
+
+ return client
+}
+
+// RandomEmail returns a unique email address for use in tests.
+func RandomEmail() string {
+ return uuid.New().String() + "@test.com"
+}
+
+// SetupWithAuth is a convenience that calls Setup and SeedClient, then returns
+// the miniredis instance and the seeded client.
+func SetupWithAuth(t *testing.T) (*miniredis.Miniredis, *data.Client) {
+ t.Helper()
+ mr := Setup(t)
+ client := SeedClient(t)
+ return mr, client
+}
+
+// SeedUser creates a test user in the database and returns it.
+func SeedUser(t *testing.T, email string, permLevel uint) *data.User {
+ t.Helper()
+ ctx := t.Context()
+ u := data.NewUser(
+ data.WithEmail(email),
+ data.WithUsername("user-"+uuid.New().String()[:8]),
+ data.WithPermissionLevel(permLevel),
+ data.WithNickname("Test User"),
+ )
+ if err := u.Create(ctx); err != nil {
+ t.Fatalf("SeedUser: create %q: %v", email, err)
+ }
+ return u
+}
diff --git a/tracer/otel_tracer.go b/tracer/otel_tracer.go
index 3f9ba1b..81b0b54 100644
--- a/tracer/otel_tracer.go
+++ b/tracer/otel_tracer.go
@@ -34,10 +34,22 @@ func Init(ctx context.Context) func(context.Context) error {
slog.Error("[OTEL] Failed to create resource", "err", err)
}
- traceExporter, _ := otlptracegrpc.New(ctx,
+ retryConfig := otlptracegrpc.RetryConfig{
+ Enabled: true,
+ InitialInterval: 1 * time.Second,
+ MaxInterval: 10 * time.Second,
+ }
+
+ // Trace Exporter
+ traceExporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
+ otlptracegrpc.WithRetry(retryConfig),
)
+ if err != nil {
+ slog.Error("[OTEL] Failed to create trace exporter", "err", err)
+ }
+
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
@@ -45,10 +57,16 @@ func Init(ctx context.Context) func(context.Context) error {
)
otel.SetTracerProvider(tp)
- metricExporter, _ := otlpmetricgrpc.New(ctx,
+ // Metric Exporter
+ metricExporter, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint(endpoint),
otlpmetricgrpc.WithInsecure(),
+ otlpmetricgrpc.WithRetry(otlpmetricgrpc.RetryConfig(retryConfig)),
)
+ if err != nil {
+ slog.Error("[OTEL] Failed to create metric exporter", "err", err)
+ }
+
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter,
@@ -56,10 +74,16 @@ func Init(ctx context.Context) func(context.Context) error {
)
otel.SetMeterProvider(mp)
- logExporter, _ := otlploggrpc.New(ctx,
+ // Log Exporter
+ logExporter, err := otlploggrpc.New(ctx,
otlploggrpc.WithEndpoint(endpoint),
otlploggrpc.WithInsecure(),
+ otlploggrpc.WithRetry(otlploggrpc.RetryConfig(retryConfig)),
)
+ if err != nil {
+ slog.Error("[OTEL] Failed to create log exporter", "err", err)
+ }
+
lp := sdklog.NewLoggerProvider(
sdklog.WithResource(res),
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
diff --git a/tracer/tracer_test.go b/tracer/tracer_test.go
new file mode 100644
index 0000000..81626c7
--- /dev/null
+++ b/tracer/tracer_test.go
@@ -0,0 +1,92 @@
+package tracer
+
+import (
+ "context"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// ---- ShortPath ----
+
+func TestShortPathWithMatch(t *testing.T) {
+ input := "/home/user/projects/cms-server/internal/foo/bar.go"
+ assert.Equal(t, "cms-server/internal/foo/bar.go", ShortPath(input))
+}
+
+func TestShortPathNoMatch(t *testing.T) {
+ input := "/home/user/other-project/main.go"
+ assert.Equal(t, input, ShortPath(input))
+}
+
+func TestShortPathEmpty(t *testing.T) {
+ assert.Equal(t, "", ShortPath(""))
+}
+
+// ---- StartSpan ----
+
+func TestStartSpanReturnsSpan(t *testing.T) {
+ viper.Set("server.application", "test-app")
+ defer viper.Reset()
+
+ ctx, span := StartSpan(context.Background(), "test_lib", "test_op")
+ require.NotNil(t, span)
+ require.NotNil(t, ctx)
+ span.End()
+}
+
+func TestStartSpanWithAdditionInfo(t *testing.T) {
+ viper.Set("server.application", "test-app")
+ defer viper.Reset()
+
+ info := AdditionSpanLayerInfo{
+ LayerName: "test-layer",
+ FuncName: "TestFunc",
+ FilePath: "/home/user/cms-server/foo.go",
+ Line: 42,
+ }
+
+ ctx, span := StartSpan(context.Background(), "test_lib", "test_op", info)
+ require.NotNil(t, span)
+ require.NotNil(t, ctx)
+ span.End()
+}
+
+func TestStartSpanContextPropagation(t *testing.T) {
+ viper.Set("server.application", "test-app")
+ defer viper.Reset()
+
+ type key struct{}
+ parent := context.WithValue(context.Background(), key{}, "sentinel")
+
+ childCtx, span := StartSpan(parent, "test_lib", "test_op")
+ defer span.End()
+
+ // Context values must be preserved across span creation.
+ assert.Equal(t, "sentinel", childCtx.Value(key{}))
+}
+
+// ---- Init / Shutdown ----
+
+func TestTracerInitReturnsShutdown(t *testing.T) {
+ viper.Set("server.application", "test-app")
+ viper.Set("server.service_name", "test-svc")
+ // Point to a non-existent endpoint so exporters fail fast without
+ // blocking the test — Init logs the errors and continues.
+ viper.Set("tracer.otel_controller_endpoint", "localhost:1")
+ defer viper.Reset()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ shutdown := Init(ctx)
+ require.NotNil(t, shutdown)
+
+ // Shutdown should complete without panicking even though exporters
+ // never connected.
+ shutCtx, shutCancel := context.WithCancel(context.Background())
+ defer shutCancel()
+ _ = shutdown(shutCtx)
+}
diff --git a/tracer/utils.go b/tracer/utils.go
new file mode 100644
index 0000000..a5ef4ce
--- /dev/null
+++ b/tracer/utils.go
@@ -0,0 +1,62 @@
+package tracer
+
+import (
+ "context"
+ "path"
+ "runtime"
+ "strings"
+
+ "github.com/spf13/viper"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+type AdditionSpanLayerInfo struct {
+ LayerName string
+ FuncName string
+ FilePath string
+ Line int
+}
+
+func StartSpan(ctx context.Context, libName, spanName string, additionSpanLayer ...AdditionSpanLayerInfo) (context.Context, trace.Span) {
+ var (
+ finalFunc string
+ finalFile string
+ finalLine int
+ finalLayer string
+ )
+
+ if len(additionSpanLayer) > 0 {
+ finalFunc = additionSpanLayer[0].FuncName
+ finalFile = additionSpanLayer[0].FilePath
+ finalLine = additionSpanLayer[0].Line
+ finalLayer = additionSpanLayer[0].LayerName
+ } else {
+ pc, file, line, ok := runtime.Caller(1)
+ if ok {
+ finalFile = file
+ finalLine = line
+ if fn := runtime.FuncForPC(pc); fn != nil {
+ finalFunc = fn.Name()
+ }
+ finalLayer = "undefined"
+ }
+ }
+
+ tracer := otel.Tracer(path.Join(viper.GetString("server.application"), libName))
+ return tracer.Start(ctx, path.Join(libName, spanName), trace.WithAttributes(
+ attribute.String("code.function", finalFunc),
+ attribute.String("code.file", ShortPath(finalFile)),
+ attribute.Int("code.line", finalLine),
+ attribute.String("trace.layer", finalLayer),
+ ))
+}
+
+func ShortPath(path string) string {
+ parts := strings.Split(path, "cms-server/")
+ if len(parts) > 1 {
+ return "cms-server/" + parts[1]
+ }
+ return path
+}
diff --git a/utils/response.go b/utils/response.go
index 381b777..a7be726 100644
--- a/utils/response.go
+++ b/utils/response.go
@@ -8,10 +8,10 @@ import (
)
type RespStatus struct {
- Code int `json:"code"`
- Status string `json:"status"`
- ErrorId string `json:"error_id"`
- Data any `json:"data"`
+ Code int `json:"code" validate:"required"`
+ Status string `json:"status" validate:"required"`
+ ErrorId string `json:"error_id" validate:"required"`
+ Data any `json:"data" validate:"required"`
}
func render(c *gin.Context, code int, errId string, data []any, abort bool) {
diff --git a/utils/response_test.go b/utils/response_test.go
new file mode 100644
index 0000000..db20edb
--- /dev/null
+++ b/utils/response_test.go
@@ -0,0 +1,78 @@
+package utils
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func init() {
+ gin.SetMode(gin.TestMode)
+}
+
+func newTestContext() (*gin.Context, *httptest.ResponseRecorder) {
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
+ return c, w
+}
+
+func TestHttpResponseNoData(t *testing.T) {
+ c, w := newTestContext()
+ HttpResponse(c, http.StatusOK, "2ep_svc100000")
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
+
+ var resp RespStatus
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ assert.Equal(t, http.StatusOK, resp.Code)
+ assert.Equal(t, "OK", resp.Status)
+ assert.Equal(t, "2ep_svc100000", resp.ErrorId)
+ assert.Nil(t, resp.Data)
+}
+
+func TestHttpResponseWithData(t *testing.T) {
+ c, w := newTestContext()
+ payload := map[string]string{"key": "value"}
+ HttpResponse(c, http.StatusOK, "2ep_svc100000", payload)
+
+ var resp RespStatus
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ data, ok := resp.Data.(map[string]any)
+ require.True(t, ok)
+ assert.Equal(t, "value", data["key"])
+}
+
+func TestHttpResponseWithMultipleData(t *testing.T) {
+ c, w := newTestContext()
+ HttpResponse(c, http.StatusOK, "code", "a", "b")
+
+ var resp RespStatus
+ require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
+ // Multiple data items are returned as a slice
+ slice, ok := resp.Data.([]any)
+ require.True(t, ok)
+ assert.Len(t, slice, 2)
+}
+
+func TestHttpResponse4xx(t *testing.T) {
+ c, w := newTestContext()
+ HttpResponse(c, http.StatusBadRequest, "errcode")
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestHttpAbortSetsAbortedFlag(t *testing.T) {
+ c, w := newTestContext()
+ HttpAbort(c, http.StatusUnauthorized, "errcode")
+
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+ // gin marks the context as aborted
+ assert.True(t, c.IsAborted())
+}