Compare commits

3 Commits

Author SHA1 Message Date
49e02d3d79 Fix gitea workflows
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Has been cancelled
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Backend Build (NixCN CMS) TeamCity build failed
Client CMS Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-27 17:54:23 +00:00
1927dd6a8c Merge pull request 'Fix gitea workflow name' (#8) from develop into main
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Has been cancelled
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Reviewed-on: nixcn/nixcn-cms#8
2026-01-27 17:48:58 +00:00
ad521e04ae Merge pull request 'First merge from develop to main (WIP)' (#7) from develop into main
Reviewed-on: nixcn/nixcn-cms#7
2026-01-27 17:47:05 +00:00
123 changed files with 1758 additions and 13221 deletions

View File

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

26
Containerfile Normal file
View File

@@ -0,0 +1,26 @@
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 mod tidy && \
go build -o /app/nixcn-cms
FROM docker.io/alpine:3.23 AS backend
WORKDIR /app
COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms
EXPOSE 8000
ENTRYPOINT [ "/app/nixcn-cms" ]

View File

@@ -1,87 +0,0 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Exchange handles the authorization code swap process.
//
// @Summary Exchange Auth Code
// @Description Exchanges client credentials and user session for a specific redirect authorization code.
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials"
// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResponse} "Successful exchange"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /auth/exchange [post]
func (self *AuthHandler) Exchange(c *gin.Context) {
var exchangeData service_auth.ExchangeData
if err := c.ShouldBindJSON(&exchangeData); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUnauthorized).
SetError(nil).
Throw(c).
String()
utils.HttpResponse(c, 401, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 500, errorCode)
return
}
result := self.svc.Exchange(&service_auth.ExchangePayload{
Context: c,
UserId: userId,
Data: &exchangeData,
})
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
}

View File

@@ -1,23 +1,8 @@
package auth
import (
"nixcn-cms/middleware"
"nixcn-cms/service/service_auth"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
svc service_auth.AuthService
}
func ApiHandler(r *gin.RouterGroup) {
authSvc := service_auth.NewAuthService()
authHandler := &AuthHandler{authSvc}
r.GET("/redirect", authHandler.Redirect)
r.POST("/magic", middleware.ApiVersionCheck(), authHandler.Magic)
r.POST("/token", middleware.ApiVersionCheck(), authHandler.Token)
r.POST("/refresh", middleware.ApiVersionCheck(), authHandler.Refresh)
r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), authHandler.Exchange)
}

View File

@@ -1,55 +0,0 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Magic handles the "Magic Link" authentication request.
//
// @Summary Request Magic Link
// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.MagicData true "Magic Link Request Data"
// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResponse} "Successful request"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /auth/magic [post]
func (self *AuthHandler) Magic(c *gin.Context) {
var magicData service_auth.MagicData
if err := c.ShouldBindJSON(&magicData); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
magicData.ClientIP = c.ClientIP()
result := self.svc.Magic(&service_auth.MagicPayload{
Context: c,
Data: &magicData,
})
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
}

View File

@@ -1,61 +0,0 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Redirect handles the post-verification callback and redirects the user to the target application.
//
// @Summary Handle Auth Callback and Redirect
// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
// @Tags Authentication
// @Accept x-www-form-urlencoded
// @Produce json
// @Produce html
// @Param client_id query string true "Client Identifier"
// @Param redirect_uri query string true "Target Redirect URI"
// @Param code query string true "Temporary Verification Code"
// @Param state query string false "Opaque state used to maintain state between the request and callback"
// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Client Not Found / URI Mismatch"
// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Verification Code"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /auth/redirect [get]
func (self *AuthHandler) Redirect(c *gin.Context) {
data := &service_auth.RedirectData{
ClientId: c.Query("client_id"),
RedirectUri: c.Query("redirect_uri"),
State: c.Query("state"),
Code: c.Query("code"),
}
if data.ClientId == "" || data.RedirectUri == "" || data.Code == "" {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Redirect(&service_auth.RedirectPayload{
Context: c,
Data: data,
})
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
c.Redirect(302, result.Data)
}

View File

@@ -1,53 +0,0 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Refresh handles the token rotation process.
//
// @Summary Refresh Access Token
// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token.
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.RefreshData true "Refresh Token Body"
// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful rotation"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 401 {object} utils.RespStatus{data=nil} "Invalid Refresh Token"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /auth/refresh [post]
func (self *AuthHandler) Refresh(c *gin.Context) {
var refreshData service_auth.RefreshData
if err := c.ShouldBindJSON(&refreshData); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Refresh(&service_auth.RefreshPayload{
Context: c,
Data: &refreshData,
})
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
}

View File

@@ -1,53 +0,0 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Token exchanges an authorization code for access and refresh tokens.
//
// @Summary Exchange Code for Token
// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.TokenData true "Token Request Body"
// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful token issuance"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Code"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Router /auth/token [post]
func (self *AuthHandler) Token(c *gin.Context) {
var tokenData service_auth.TokenData
if err := c.ShouldBindJSON(&tokenData); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Token(&service_auth.TokenPayload{
Context: c,
Data: &tokenData,
})
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
}

View File

@@ -1,124 +0,0 @@
package event
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Checkin generates a check-in code for a specific event.
//
// @Summary Generate Check-in Code
// @Description Creates a temporary check-in code for the authenticated user and event.
// @Tags Event
// @Accept json
// @Produce json
// @Param event_id query string true "Event UUID"
// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Security ApiKeyAuth
// @Router /event/checkin [get]
func (self *EventHandler) Checkin(c *gin.Context) {
userIdOrig, _ := c.Get("user_id")
userId, _ := uuid.Parse(userIdOrig.(string))
eventIdOrig := c.Query("event_id")
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.Checkin(&service_event.CheckinPayload{
Context: c,
UserId: userId,
Data: &service_event.CheckinData{EventId: eventId},
})
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}
// CheckinSubmit validates a check-in code to complete attendance.
//
// @Summary Submit Check-in Code
// @Description Submits the generated code to mark the user as attended.
// @Tags Event
// @Accept json
// @Produce json
// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data"
// @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input"
// @Security ApiKeyAuth
// @Router /event/checkin/submit [post]
func (self *EventHandler) CheckinSubmit(c *gin.Context) {
var data service_event.CheckinSubmitData
if err := c.ShouldBindJSON(&data); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.CheckinSubmit(&service_event.CheckinSubmitPayload{
Context: c,
Data: &data,
})
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
}
// CheckinQuery retrieves the check-in status of a user for an event.
//
// @Summary Query Check-in Status
// @Description Returns the timestamp of when the user checked in, or null if not yet checked in.
// @Tags Event
// @Accept json
// @Produce json
// @Param event_id query string true "Event UUID"
// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found"
// @Security ApiKeyAuth
// @Router /event/checkin/query [get]
func (self *EventHandler) CheckinQuery(c *gin.Context) {
userIdOrig, _ := c.Get("user_id")
userId, _ := uuid.Parse(userIdOrig.(string))
eventIdOrig := c.Query("event_id")
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.CheckinQuery(&service_event.CheckinQueryPayload{
Context: c,
UserId: userId,
Data: &service_event.CheckinQueryData{EventId: eventId},
})
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

View File

@@ -2,22 +2,10 @@ package event
import (
"nixcn-cms/middleware"
"nixcn-cms/service/service_event"
"github.com/gin-gonic/gin"
)
type EventHandler struct {
svc service_event.EventService
}
func ApiHandler(r *gin.RouterGroup) {
eventSvc := service_event.NewEventService()
eventHandler := &EventHandler{eventSvc}
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
r.GET("/info", eventHandler.Info)
r.GET("/checkin", eventHandler.Checkin)
r.GET("/checkin/query", eventHandler.CheckinQuery)
r.POST("/checkin/submit", middleware.Permission(20), eventHandler.CheckinSubmit)
}

View File

@@ -1,57 +0,0 @@
package event
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_event"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Info retrieves basic information about a specific event.
//
// @Summary Get Event Information
// @Description Fetches the name, start time, and end time of an event using its UUID.
// @Tags Event
// @Accept json
// @Produce json
// @Param event_id query string true "Event UUID"
// @Success 200 {object} utils.RespStatus{data=service_event.InfoResponse} "Successful retrieval"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
// @Security ApiKeyAuth
// @Router /event/info [get]
func (self *EventHandler) Info(c *gin.Context) {
eventIdOrig := c.Query("event_id")
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 500, errorCode)
return
}
result := self.svc.Info(&service_event.InfoPayload{
Context: c,
Data: &service_event.InfoData{
EventId: eventId,
},
})
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
}

View File

@@ -2,25 +2,14 @@ package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"nixcn-cms/service"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Full retrieves the complete list of users directly from the database table.
//
// @Summary Get Full User Table
// @Description Fetches all user records without pagination. This is typically used for administrative overview or data export.
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {object} utils.RespStatus{data=service_user.UserTableResponse} "Successful retrieval of full user table"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error)"
// @Security ApiKeyAuth
// @Router /user/full [get]
func (self *UserHandler) Full(c *gin.Context) {
userTablePayload := &service_user.UserTablePayload{
userTablePayload := &service.UserTablePayload{
Context: c,
}

View File

@@ -2,17 +2,17 @@ package user
import (
"nixcn-cms/middleware"
"nixcn-cms/service/service_user"
"nixcn-cms/service"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
svc service_user.UserService
svc service.UserService
}
func ApiHandler(r *gin.RouterGroup) {
userSvc := service_user.NewUserService()
userSvc := service.NewUserService()
userHandler := &UserHandler{userSvc}
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(5))

View File

@@ -2,26 +2,13 @@ package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"nixcn-cms/service"
"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
// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)"
// @Security ApiKeyAuth
// @Router /user/info [get]
func (self *UserHandler) Info(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
@@ -52,7 +39,7 @@ func (self *UserHandler) Info(c *gin.Context) {
return
}
UserInfoPayload := &service_user.UserInfoPayload{
UserInfoPayload := &service.UserInfoPayload{
Context: c,
UserId: userId,
Data: nil,

View File

@@ -2,26 +2,12 @@ package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"nixcn-cms/service"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// List retrieves a paginated list of users from the search engine.
//
// @Summary List Users
// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
// @Tags User
// @Accept json
// @Produce json
// @Param limit query string false "Maximum number of users to return (default 0)"
// @Param offset query string true "Number of users to skip"
// @Success 200 {object} utils.RespStatus{data=[]data.UserSearchDoc} "Successful paginated list retrieval"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)"
// @Security ApiKeyAuth
// @Router /user/list [get]
func (self *UserHandler) List(c *gin.Context) {
type ListQuery struct {
Limit *string `form:"limit"`
@@ -43,7 +29,7 @@ func (self *UserHandler) List(c *gin.Context) {
return
}
userListPayload := &service_user.UserListPayload{
userListPayload := &service.UserListPayload{
Context: c,
Limit: query.Limit,
Offset: query.Offset,

View File

@@ -2,28 +2,13 @@ package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"nixcn-cms/service"
"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
// @Param payload body service_user.UserInfoData true "Updated User Profile Data"
// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update"
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)"
// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)"
// @Security ApiKeyAuth
// @Router /user/update [patch]
func (self *UserHandler) Update(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
@@ -53,7 +38,7 @@ func (self *UserHandler) Update(c *gin.Context) {
return
}
userInfoPayload := &service_user.UserInfoPayload{
userInfoPayload := &service.UserInfoPayload{
Context: c,
UserId: userId,
}

View File

@@ -24,6 +24,3 @@ dist-ssr
*.sw?
.direnv
*storybook.log
storybook-static

View File

@@ -1,17 +0,0 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs",
"@storybook/addon-onboarding"
],
"framework": "@storybook/react-vite"
};
export default config;

View File

@@ -1,24 +0,0 @@
import type { Preview } from '@storybook/react-vite';
import { ThemeProvider } from '../src/components/theme-provider';
import '../src/index.css';
const preview: Preview = {
decorators: [(Story) => <ThemeProvider defaultTheme="dark"><Story /></ThemeProvider >],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo'
}
},
};
export default preview;

View File

@@ -1,7 +0,0 @@
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
import { setProjectAnnotations } from '@storybook/react-vite';
import * as projectAnnotations from './preview';
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);

View File

@@ -1,10 +1,9 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
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/**/*', 'src/client/**/*', 'openapi-ts.config.ts'],
ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*'],
react: true,
stylistic: {
semi: true,

View File

@@ -1,19 +0,0 @@
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: 'http://10.0.0.10:8000/swagger/doc.json', // sign up at app.heyapi.dev
output: 'src/client',
plugins: [
'@hey-api/typescript',
'@tanstack/react-query',
'zod',
{
name: '@hey-api/transformers',
dates: true,
},
{
name: '@hey-api/sdk',
transformer: true,
},
],
});

View File

@@ -3,21 +3,13 @@
"type": "module",
"version": "0.0.0",
"private": true,
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"gen": "openapi-ts",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"preview": "vite preview"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@dicebear/collection": "^9.3.1",
"@dicebear/core": "^9.3.1",
"@dicebear/identicon": "^9.3.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@@ -32,7 +24,6 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
@@ -60,7 +51,6 @@
"qrcode": "^1.5.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.1.0",
"react-hook-form": "^7.69.0",
"react-markdown": "^10.1.0",
"recharts": "2.15.4",
@@ -74,16 +64,8 @@
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.1",
"@chromatic-com/storybook": "^5.0.0",
"@eslint-react/eslint-plugin": "^2.3.13",
"@eslint/js": "^9.39.1",
"@hey-api/openapi-ts": "0.91.0",
"@storybook/addon-a11y": "^10.2.3",
"@storybook/addon-docs": "^10.2.3",
"@storybook/addon-onboarding": "^10.2.3",
"@storybook/addon-themes": "^10.2.3",
"@storybook/addon-vitest": "^10.2.3",
"@storybook/react-vite": "^10.2.3",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/eslint-plugin-query": "^5.91.2",
"@tanstack/router-plugin": "^1.141.7",
@@ -96,29 +78,24 @@
"@types/react-dom": "^19.2.3",
"@types/utf8": "^3.0.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-storybook": "^10.2.3",
"globals": "^16.5.0",
"lint-staged": "^16.2.7",
"playwright": "^1.58.0",
"simple-git-hooks": "^2.13.1",
"storybook": "^10.2.3",
"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",
"vitest": "^4.0.18"
"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"
}

2113
client/cms/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,402 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
import { client } from '../client.gen';
import { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from '../sdk.gen';
import type { GetAuthRedirectData, GetAuthRedirectError, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetEventListData, GetEventListError, GetEventListResponse, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse } from '../types.gen';
/**
* Exchange Auth Code
*
* Exchanges client credentials and user session for a specific redirect authorization code.
*/
export const postAuthExchangeMutation = (options?: Partial<Options<PostAuthExchangeData>>): UseMutationOptions<PostAuthExchangeResponse, PostAuthExchangeError, Options<PostAuthExchangeData>> => {
const mutationOptions: UseMutationOptions<PostAuthExchangeResponse, PostAuthExchangeError, Options<PostAuthExchangeData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postAuthExchange({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Request Magic Link
*
* Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
*/
export const postAuthMagicMutation = (options?: Partial<Options<PostAuthMagicData>>): UseMutationOptions<PostAuthMagicResponse, PostAuthMagicError, Options<PostAuthMagicData>> => {
const mutationOptions: UseMutationOptions<PostAuthMagicResponse, PostAuthMagicError, Options<PostAuthMagicData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postAuthMagic({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export type QueryKey<TOptions extends Options> = [
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
_id: string;
_infinite?: boolean;
tags?: ReadonlyArray<string>;
}
];
const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray<string>): [
QueryKey<TOptions>[0]
] => {
const params: QueryKey<TOptions>[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey<TOptions>[0];
if (infinite) {
params._infinite = infinite;
}
if (tags) {
params.tags = tags;
}
if (options?.body) {
params.body = options.body;
}
if (options?.headers) {
params.headers = options.headers;
}
if (options?.path) {
params.path = options.path;
}
if (options?.query) {
params.query = options.query;
}
return [params];
};
export const getAuthRedirectQueryKey = (options: Options<GetAuthRedirectData>) => createQueryKey('getAuthRedirect', options);
/**
* Handle Auth Callback and Redirect
*
* Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
*/
export const getAuthRedirectOptions = (options: Options<GetAuthRedirectData>) => queryOptions<unknown, GetAuthRedirectError, unknown, ReturnType<typeof getAuthRedirectQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getAuthRedirect({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getAuthRedirectQueryKey(options)
});
/**
* Refresh Access Token
*
* Accepts a valid refresh token to issue a new access token and a rotated refresh token.
*/
export const postAuthRefreshMutation = (options?: Partial<Options<PostAuthRefreshData>>): UseMutationOptions<PostAuthRefreshResponse, PostAuthRefreshError, Options<PostAuthRefreshData>> => {
const mutationOptions: UseMutationOptions<PostAuthRefreshResponse, PostAuthRefreshError, Options<PostAuthRefreshData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postAuthRefresh({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Exchange Code for Token
*
* Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
*/
export const postAuthTokenMutation = (options?: Partial<Options<PostAuthTokenData>>): UseMutationOptions<PostAuthTokenResponse, PostAuthTokenError, Options<PostAuthTokenData>> => {
const mutationOptions: UseMutationOptions<PostAuthTokenResponse, PostAuthTokenError, Options<PostAuthTokenData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postAuthToken({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getEventCheckinQueryKey = (options: Options<GetEventCheckinData>) => createQueryKey('getEventCheckin', options);
/**
* Generate Check-in Code
*
* Creates a temporary check-in code for the authenticated user and event.
*/
export const getEventCheckinOptions = (options: Options<GetEventCheckinData>) => queryOptions<GetEventCheckinResponse, GetEventCheckinError, GetEventCheckinResponse, ReturnType<typeof getEventCheckinQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getEventCheckin({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getEventCheckinQueryKey(options)
});
export const getEventCheckinQueryQueryKey = (options: Options<GetEventCheckinQueryData>) => createQueryKey('getEventCheckinQuery', options);
/**
* Query Check-in Status
*
* Returns the timestamp of when the user checked in, or null if not yet checked in.
*/
export const getEventCheckinQueryOptions = (options: Options<GetEventCheckinQueryData>) => queryOptions<GetEventCheckinQueryResponse, GetEventCheckinQueryError, GetEventCheckinQueryResponse, ReturnType<typeof getEventCheckinQueryQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getEventCheckinQuery({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getEventCheckinQueryQueryKey(options)
});
/**
* Submit Check-in Code
*
* Submits the generated code to mark the user as attended.
*/
export const postEventCheckinSubmitMutation = (options?: Partial<Options<PostEventCheckinSubmitData>>): UseMutationOptions<PostEventCheckinSubmitResponse, PostEventCheckinSubmitError, Options<PostEventCheckinSubmitData>> => {
const mutationOptions: UseMutationOptions<PostEventCheckinSubmitResponse, PostEventCheckinSubmitError, Options<PostEventCheckinSubmitData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postEventCheckinSubmit({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getEventInfoQueryKey = (options: Options<GetEventInfoData>) => createQueryKey('getEventInfo', options);
/**
* Get Event Information
*
* Fetches the name, start time, and end time of an event using its UUID.
*/
export const getEventInfoOptions = (options: Options<GetEventInfoData>) => queryOptions<GetEventInfoResponse, GetEventInfoError, GetEventInfoResponse, ReturnType<typeof getEventInfoQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getEventInfo({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getEventInfoQueryKey(options)
});
export const getEventListQueryKey = (options: Options<GetEventListData>) => createQueryKey('getEventList', options);
/**
* List Events
*
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
*/
export const getEventListOptions = (options: Options<GetEventListData>) => queryOptions<GetEventListResponse, GetEventListError, GetEventListResponse, ReturnType<typeof getEventListQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getEventList({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getEventListQueryKey(options)
});
const createInfiniteParams = <K extends Pick<QueryKey<Options>[0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey<Options>, page: K) => {
const params = { ...queryKey[0] };
if (page.body) {
params.body = {
...queryKey[0].body as any,
...page.body as any
};
}
if (page.headers) {
params.headers = {
...queryKey[0].headers,
...page.headers
};
}
if (page.path) {
params.path = {
...queryKey[0].path as any,
...page.path as any
};
}
if (page.query) {
params.query = {
...queryKey[0].query as any,
...page.query as any
};
}
return params as unknown as typeof page;
};
export const getEventListInfiniteQueryKey = (options: Options<GetEventListData>): QueryKey<Options<GetEventListData>> => createQueryKey('getEventList', options, true);
/**
* List Events
*
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
*/
export const getEventListInfiniteOptions = (options: Options<GetEventListData>) => infiniteQueryOptions<GetEventListResponse, GetEventListError, InfiniteData<GetEventListResponse>, QueryKey<Options<GetEventListData>>, string | Pick<QueryKey<Options<GetEventListData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
// @ts-ignore
const page: Pick<QueryKey<Options<GetEventListData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : {
query: {
offset: pageParam
}
};
const params = createInfiniteParams(queryKey, page);
const { data } = await getEventList({
...options,
...params,
signal,
throwOnError: true
});
return data;
},
queryKey: getEventListInfiniteQueryKey(options)
});
export const getUserInfoQueryKey = (options?: Options<GetUserInfoData>) => createQueryKey('getUserInfo', options);
/**
* Get My User Information
*
* Fetches the complete profile data for the user associated with the provided session/token.
*/
export const getUserInfoOptions = (options?: Options<GetUserInfoData>) => queryOptions<GetUserInfoResponse, GetUserInfoError, GetUserInfoResponse, ReturnType<typeof getUserInfoQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getUserInfo({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getUserInfoQueryKey(options)
});
export const getUserInfoByUserIdQueryKey = (options: Options<GetUserInfoByUserIdData>) => createQueryKey('getUserInfoByUserId', options);
/**
* Get Other User Information
*
* Fetches the complete profile data for the user associated with the provided session/token.
*/
export const getUserInfoByUserIdOptions = (options: Options<GetUserInfoByUserIdData>) => queryOptions<GetUserInfoByUserIdResponse, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, ReturnType<typeof getUserInfoByUserIdQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getUserInfoByUserId({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getUserInfoByUserIdQueryKey(options)
});
export const getUserListQueryKey = (options: Options<GetUserListData>) => createQueryKey('getUserList', options);
/**
* List Users
*
* Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
*/
export const getUserListOptions = (options: Options<GetUserListData>) => queryOptions<GetUserListResponse, GetUserListError, GetUserListResponse, ReturnType<typeof getUserListQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getUserList({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getUserListQueryKey(options)
});
export const getUserListInfiniteQueryKey = (options: Options<GetUserListData>): QueryKey<Options<GetUserListData>> => createQueryKey('getUserList', options, true);
/**
* List Users
*
* Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
*/
export const getUserListInfiniteOptions = (options: Options<GetUserListData>) => infiniteQueryOptions<GetUserListResponse, GetUserListError, InfiniteData<GetUserListResponse>, QueryKey<Options<GetUserListData>>, string | Pick<QueryKey<Options<GetUserListData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
// @ts-ignore
const page: Pick<QueryKey<Options<GetUserListData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : {
query: {
offset: pageParam
}
};
const params = createInfiniteParams(queryKey, page);
const { data } = await getUserList({
...options,
...params,
signal,
throwOnError: true
});
return data;
},
queryKey: getUserListInfiniteQueryKey(options)
});
/**
* Update User Information
*
* Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
* Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
*/
export const patchUserUpdateMutation = (options?: Partial<Options<PatchUserUpdateData>>): UseMutationOptions<PatchUserUpdateResponse, PatchUserUpdateError, Options<PatchUserUpdateData>> => {
const mutationOptions: UseMutationOptions<PatchUserUpdateResponse, PatchUserUpdateError, Options<PatchUserUpdateData>> = {
mutationFn: async (fnOptions) => {
const { data } = await patchUserUpdate({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};

View File

@@ -1,16 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:8000/api/v1' }));

View File

@@ -1,311 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type {
Client,
Config,
RequestOptions,
ResolvedRequestOptions,
} from './types.gen';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils.gen';
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
ResolvedRequestOptions
>();
const beforeRequest = async (options: RequestOptions) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined,
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body);
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
const url = buildUrl(opts);
return { opts, url };
};
const request: Client['request'] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response: Response;
try {
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(
error,
undefined as any,
request,
opts,
)) as unknown;
}
}
finalError = finalError || ({} as unknown);
if (opts.throwOnError) {
throw finalError;
}
// Return error response
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
request,
response: undefined as any,
};
}
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
if (
response.status === 204 ||
response.headers.get('Content-Length') === '0'
) {
let emptyData: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
break;
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
};
}
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'text':
data = await response[parseAs]();
break;
case 'json': {
// Some servers return 200 with no Content-Length and empty body.
// response.json() would throw; read as text and parse if non-empty.
const text = await response.text();
data = text ? JSON.parse(text) : {};
break;
}
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
const makeMethodFn =
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn =
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
return request;
},
serializedBody: getValidRequestBody(opts) as
| BodyInit
| null
| undefined,
url,
});
};
return {
buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};

View File

@@ -1,25 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from './types.gen';
export { createConfig, mergeHeaders } from './utils.gen';

View File

@@ -1,241 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from '../core/auth.gen';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types.gen';
import type { Middleware } from './utils.gen';
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T['baseUrl'];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?:
| 'arrayBuffer'
| 'auto'
| 'blob'
| 'formData'
| 'json'
| 'stream'
| 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
}
export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}>,
Pick<
ServerSentEventsOptions<TData>,
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
| 'sseMaxRetryAttempts'
| 'sseMaxRetryDelay'
> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
serializedBody?: string;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
? Promise<
TResponseStyle extends 'data'
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends 'data'
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
'method'
>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: TData & Options<TData>,
) => string;
export type Client = CoreClient<
RequestFn,
Config,
MethodFn,
BuildUrlFn,
SseFn
> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
([TData] extends [never] ? unknown : Omit<TData, 'url'>);

View File

@@ -1,332 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from '../core/bodySerializer.gen';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import { getUrl } from '../core/utils.gen';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
export const createQuerySerializer = <T = unknown>({
parameters = {},
...args
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
const options = parameters[name] || args;
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'form',
value,
...options.array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...options.object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved: options.allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return 'stream';
}
const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) {
return;
}
if (
cleanContent.startsWith('application/json') ||
cleanContent.endsWith('+json')
) {
return 'json';
}
if (cleanContent === 'multipart/form-data') {
return 'formData';
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) =>
cleanContent.startsWith(type),
)
) {
return 'blob';
}
if (cleanContent.startsWith('text/')) {
return 'text';
}
return;
};
const checkForExistence = (
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false;
}
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue;
}
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query':
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
}
};
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
entries.push([key, value]);
});
return entries;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header) {
continue;
}
const iterator =
header instanceof Headers
? headersEntries(header)
: Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
fns: Array<Interceptor | null> = [];
clear(): void {
this.fns = [];
}
eject(id: number | Interceptor): void {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = null;
}
}
exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id);
return Boolean(this.fns[index]);
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === 'number') {
return this.fns[id] ? id : -1;
}
return this.fns.indexOf(id);
}
update(
id: number | Interceptor,
fn: Interceptor,
): number | Interceptor | false {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = fn;
return id;
}
return false;
}
use(fn: Interceptor): number {
this.fns.push(fn);
return this.fns.length - 1;
}
}
export interface Middleware<Req, Res, Err, Options> {
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
request: Interceptors<ReqInterceptor<Req, Options>>;
response: Interceptors<ResInterceptor<Res, Req, Options>>;
}
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
Req,
Res,
Err,
Options
> => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: 'auto',
querySerializer: defaultQuerySerializer,
...override,
});

View File

@@ -1,42 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token;
};

View File

@@ -1,100 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
type QuerySerializerOptionsObject = {
allowReserved?: boolean;
array?: Partial<SerializerOptions<ArrayStyle>>;
object?: Partial<SerializerOptions<ObjectStyle>>;
};
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/**
* Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names.
*/
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (
data: FormData,
key: string,
value: unknown,
): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};

View File

@@ -1,176 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
}
| {
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
| {
in: Slot;
map?: string;
}
| {
in?: never;
map: Slot;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if ('key' in config) {
map.set(config.key, {
map: config.map,
});
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg;
}
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
if (field.in) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value;
}
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};

View File

@@ -1,181 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ',';
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
throw new Error(
'Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(',');
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};

View File

@@ -1,136 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* JSON-friendly union that mirrors what Pinia Colada can hash.
*/
export type JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
/**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (
value === undefined ||
typeof value === 'function' ||
typeof value === 'symbol'
) {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
return value;
};
/**
* Safely stringifies a value and parses it back into a JsonValue.
*/
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try {
const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) {
return undefined;
}
return JSON.parse(json) as JsonValue;
} catch {
return undefined;
}
};
/**
* Detects plain objects (including objects with a null prototype).
*/
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null;
};
/**
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) =>
a.localeCompare(b),
);
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
const existing = result[key];
if (existing === undefined) {
result[key] = value;
continue;
}
if (Array.isArray(existing)) {
(existing as string[]).push(value);
} else {
result[key] = [existing, value];
}
}
return result;
};
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (
value: unknown,
): JsonValue | undefined => {
if (value === null) {
return null;
}
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}
if (
value === undefined ||
typeof value === 'function' ||
typeof value === 'symbol'
) {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value)) {
return stringifyToJsonValue(value);
}
if (
typeof URLSearchParams !== 'undefined' &&
value instanceof URLSearchParams
) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) {
return stringifyToJsonValue(value);
}
return undefined;
};

View File

@@ -1,266 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<
RequestInit,
'method'
> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Implementing clients can call request interceptors inside this hook.
*/
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void;
serializedBody?: RequestInit['body'];
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<
TData = unknown,
TReturn = void,
TNext = unknown,
> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
};
export const createSseClient = <TData = unknown>({
onRequest,
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined;
const sleep =
sseSleepFn ??
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break;
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set('Last-Event-ID', lastEventId);
}
try {
const requestInit: RequestInit = {
redirect: 'follow',
...options,
body: options.serializedBody,
headers,
signal,
};
let request = new Request(url, requestInit);
if (onRequest) {
request = await onRequest(url, requestInit);
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok)
throw new Error(
`SSE failed: ${response.status} ${response.statusText}`,
);
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = '';
const abortHandler = () => {
try {
reader.cancel();
} catch {
// noop
}
};
signal.addEventListener('abort', abortHandler);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
// Normalize line endings: CRLF -> LF, then CR -> LF
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
const lines = chunk.split('\n');
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(
line.replace(/^retry:\s*/, ''),
10,
);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
}
}
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join('\n');
try {
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data);
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
});
if (dataLines.length) {
yield data as any;
}
}
}
} finally {
signal.removeEventListener('abort', abortHandler);
reader.releaseLock();
}
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error);
if (
sseMaxRetryAttempts !== undefined &&
attempt >= sseMaxRetryAttempts
) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(
retryDelay * 2 ** (attempt - 1),
sseMaxRetryDelay ?? 30000,
);
await sleep(backoff);
}
}
};
const stream = createStream();
return { stream };
};

View File

@@ -1,118 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer.gen';
export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
SseFn = never,
> = {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never]
? { sse?: never }
: { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?: Uppercase<HttpMethod>;
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true
: [T] extends [never | undefined]
? [undefined] extends [T]
? false
: true
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
};

View File

@@ -1,143 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from './pathSerializer.gen';
export interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export function getValidRequestBody(options: {
body?: unknown;
bodySerializer?: BodySerializer | null;
serializedBody?: unknown;
}) {
const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) {
if ('serializedBody' in options) {
const hasSerializedBody =
options.serializedBody !== undefined && options.serializedBody !== '';
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e. client-axios)
return options.body !== '' ? options.body : null;
}
// plain/text body
if (hasBody) {
return options.body;
}
// no body was provided
return undefined;
}

View File

@@ -1,4 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from './sdk.gen';
export type { ClientOptions, DataEventIndexDoc, DataUserIndexDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetEventListData, GetEventListError, GetEventListErrors, GetEventListResponse, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponse, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceUserUserInfoData, UtilsRespStatus } from './types.gen';

View File

@@ -1,160 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetEventListData, GetEventListErrors, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
};
/**
* Exchange Auth Code
*
* Exchanges client credentials and user session for a specific redirect authorization code.
*/
export const postAuthExchange = <ThrowOnError extends boolean = false>(options: Options<PostAuthExchangeData, ThrowOnError>) => (options.client ?? client).post<PostAuthExchangeResponses, PostAuthExchangeErrors, ThrowOnError>({
url: '/auth/exchange',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Request Magic Link
*
* Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
*/
export const postAuthMagic = <ThrowOnError extends boolean = false>(options: Options<PostAuthMagicData, ThrowOnError>) => (options.client ?? client).post<PostAuthMagicResponses, PostAuthMagicErrors, ThrowOnError>({
url: '/auth/magic',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Handle Auth Callback and Redirect
*
* Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
*/
export const getAuthRedirect = <ThrowOnError extends boolean = false>(options: Options<GetAuthRedirectData, ThrowOnError>) => (options.client ?? client).get<unknown, GetAuthRedirectErrors, ThrowOnError>({ url: '/auth/redirect', ...options });
/**
* Refresh Access Token
*
* Accepts a valid refresh token to issue a new access token and a rotated refresh token.
*/
export const postAuthRefresh = <ThrowOnError extends boolean = false>(options: Options<PostAuthRefreshData, ThrowOnError>) => (options.client ?? client).post<PostAuthRefreshResponses, PostAuthRefreshErrors, ThrowOnError>({
url: '/auth/refresh',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Exchange Code for Token
*
* Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
*/
export const postAuthToken = <ThrowOnError extends boolean = false>(options: Options<PostAuthTokenData, ThrowOnError>) => (options.client ?? client).post<PostAuthTokenResponses, PostAuthTokenErrors, ThrowOnError>({
url: '/auth/token',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Generate Check-in Code
*
* Creates a temporary check-in code for the authenticated user and event.
*/
export const getEventCheckin = <ThrowOnError extends boolean = false>(options: Options<GetEventCheckinData, ThrowOnError>) => (options.client ?? client).get<GetEventCheckinResponses, GetEventCheckinErrors, ThrowOnError>({ url: '/event/checkin', ...options });
/**
* Query Check-in Status
*
* Returns the timestamp of when the user checked in, or null if not yet checked in.
*/
export const getEventCheckinQuery = <ThrowOnError extends boolean = false>(options: Options<GetEventCheckinQueryData, ThrowOnError>) => (options.client ?? client).get<GetEventCheckinQueryResponses, GetEventCheckinQueryErrors, ThrowOnError>({ url: '/event/checkin/query', ...options });
/**
* Submit Check-in Code
*
* Submits the generated code to mark the user as attended.
*/
export const postEventCheckinSubmit = <ThrowOnError extends boolean = false>(options: Options<PostEventCheckinSubmitData, ThrowOnError>) => (options.client ?? client).post<PostEventCheckinSubmitResponses, PostEventCheckinSubmitErrors, ThrowOnError>({
url: '/event/checkin/submit',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get Event Information
*
* Fetches the name, start time, and end time of an event using its UUID.
*/
export const getEventInfo = <ThrowOnError extends boolean = false>(options: Options<GetEventInfoData, ThrowOnError>) => (options.client ?? client).get<GetEventInfoResponses, GetEventInfoErrors, ThrowOnError>({ url: '/event/info', ...options });
/**
* List Events
*
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
*/
export const getEventList = <ThrowOnError extends boolean = false>(options: Options<GetEventListData, ThrowOnError>) => (options.client ?? client).get<GetEventListResponses, GetEventListErrors, ThrowOnError>({ url: '/event/list', ...options });
/**
* Get My User Information
*
* Fetches the complete profile data for the user associated with the provided session/token.
*/
export const getUserInfo = <ThrowOnError extends boolean = false>(options?: Options<GetUserInfoData, ThrowOnError>) => (options?.client ?? client).get<GetUserInfoResponses, GetUserInfoErrors, ThrowOnError>({ url: '/user/info', ...options });
/**
* Get Other User Information
*
* Fetches the complete profile data for the user associated with the provided session/token.
*/
export const getUserInfoByUserId = <ThrowOnError extends boolean = false>(options: Options<GetUserInfoByUserIdData, ThrowOnError>) => (options.client ?? client).get<GetUserInfoByUserIdResponses, GetUserInfoByUserIdErrors, ThrowOnError>({ url: '/user/info/{user_id}', ...options });
/**
* List Users
*
* Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
*/
export const getUserList = <ThrowOnError extends boolean = false>(options: Options<GetUserListData, ThrowOnError>) => (options.client ?? client).get<GetUserListResponses, GetUserListErrors, ThrowOnError>({ url: '/user/list', ...options });
/**
* Update User Information
*
* Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
* Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
*/
export const patchUserUpdate = <ThrowOnError extends boolean = false>(options: Options<PatchUserUpdateData, ThrowOnError>) => (options.client ?? client).patch<PatchUserUpdateResponses, PatchUserUpdateErrors, ThrowOnError>({
url: '/user/update',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});

View File

@@ -1,808 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: 'http://localhost:8000/api/v1' | 'https://localhost:8000/api/v1' | (string & {});
};
export type DataEventIndexDoc = {
description?: string;
end_time?: string;
event_id?: string;
name?: string;
start_time?: string;
thumbnail?: string;
type?: string;
};
export type DataUserIndexDoc = {
avatar?: string;
email?: string;
nickname?: string;
subtitle?: string;
type?: string;
user_id?: string;
username?: string;
};
export type ServiceAuthExchangeData = {
client_id?: string;
redirect_uri?: string;
state?: string;
};
export type ServiceAuthExchangeResponse = {
redirect_uri?: string;
};
export type ServiceAuthMagicData = {
client_id?: string;
client_ip?: string;
email?: string;
redirect_uri?: string;
state?: string;
turnstile_token?: string;
};
export type ServiceAuthMagicResponse = {
uri?: string;
};
export type ServiceAuthRefreshData = {
refresh_token?: string;
};
export type ServiceAuthTokenData = {
code?: string;
};
export type ServiceAuthTokenResponse = {
access_token?: string;
refresh_token?: string;
};
export type ServiceEventCheckinQueryResponse = {
checkin_at?: string;
};
export type ServiceEventCheckinResponse = {
checkin_code?: string;
};
export type ServiceEventCheckinSubmitData = {
checkin_code?: string;
};
export type ServiceUserUserInfoData = {
allow_public?: boolean;
avatar?: string;
bio?: string;
email?: string;
nickname?: string;
permission_level?: number;
subtitle?: string;
user_id?: string;
username?: string;
};
export type UtilsRespStatus = {
code?: number;
data?: unknown;
error_id?: string;
status?: string;
};
export type PostAuthExchangeData = {
/**
* Exchange Request Credentials
*/
body: ServiceAuthExchangeData;
path?: never;
query?: never;
url: '/auth/exchange';
};
export type PostAuthExchangeErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PostAuthExchangeError = PostAuthExchangeErrors[keyof PostAuthExchangeErrors];
export type PostAuthExchangeResponses = {
/**
* Successful exchange
*/
200: UtilsRespStatus & {
data?: ServiceAuthExchangeResponse;
};
};
export type PostAuthExchangeResponse = PostAuthExchangeResponses[keyof PostAuthExchangeResponses];
export type PostAuthMagicData = {
/**
* Magic Link Request Data
*/
body: ServiceAuthMagicData;
path?: never;
query?: never;
url: '/auth/magic';
};
export type PostAuthMagicErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Turnstile Verification Failed
*/
403: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PostAuthMagicError = PostAuthMagicErrors[keyof PostAuthMagicErrors];
export type PostAuthMagicResponses = {
/**
* Successful request
*/
200: UtilsRespStatus & {
data?: ServiceAuthMagicResponse;
};
};
export type PostAuthMagicResponse = PostAuthMagicResponses[keyof PostAuthMagicResponses];
export type GetAuthRedirectData = {
body?: never;
path?: never;
query: {
/**
* Client Identifier
*/
client_id: string;
/**
* Target Redirect URI
*/
redirect_uri: string;
/**
* Temporary Verification Code
*/
code: string;
/**
* Opaque state used to maintain state between the request and callback
*/
state?: string;
};
url: '/auth/redirect';
};
export type GetAuthRedirectErrors = {
/**
* Invalid Input / Client Not Found / URI Mismatch
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Invalid or Expired Verification Code
*/
403: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetAuthRedirectError = GetAuthRedirectErrors[keyof GetAuthRedirectErrors];
export type PostAuthRefreshData = {
/**
* Refresh Token Body
*/
body: ServiceAuthRefreshData;
path?: never;
query?: never;
url: '/auth/refresh';
};
export type PostAuthRefreshErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Invalid Refresh Token
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PostAuthRefreshError = PostAuthRefreshErrors[keyof PostAuthRefreshErrors];
export type PostAuthRefreshResponses = {
/**
* Successful rotation
*/
200: UtilsRespStatus & {
data?: ServiceAuthTokenResponse;
};
};
export type PostAuthRefreshResponse = PostAuthRefreshResponses[keyof PostAuthRefreshResponses];
export type PostAuthTokenData = {
/**
* Token Request Body
*/
body: ServiceAuthTokenData;
path?: never;
query?: never;
url: '/auth/token';
};
export type PostAuthTokenErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Invalid or Expired Code
*/
403: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PostAuthTokenError = PostAuthTokenErrors[keyof PostAuthTokenErrors];
export type PostAuthTokenResponses = {
/**
* Successful token issuance
*/
200: UtilsRespStatus & {
data?: ServiceAuthTokenResponse;
};
};
export type PostAuthTokenResponse = PostAuthTokenResponses[keyof PostAuthTokenResponses];
export type GetEventCheckinData = {
body?: never;
path?: never;
query: {
/**
* Event UUID
*/
event_id: string;
};
url: '/event/checkin';
};
export type GetEventCheckinErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetEventCheckinError = GetEventCheckinErrors[keyof GetEventCheckinErrors];
export type GetEventCheckinResponses = {
/**
* Successfully generated code
*/
200: UtilsRespStatus & {
data?: ServiceEventCheckinResponse;
};
};
export type GetEventCheckinResponse = GetEventCheckinResponses[keyof GetEventCheckinResponses];
export type GetEventCheckinQueryData = {
body?: never;
path?: never;
query: {
/**
* Event UUID
*/
event_id: string;
};
url: '/event/checkin/query';
};
export type GetEventCheckinQueryErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Record Not Found
*/
404: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetEventCheckinQueryError = GetEventCheckinQueryErrors[keyof GetEventCheckinQueryErrors];
export type GetEventCheckinQueryResponses = {
/**
* Current attendance status
*/
200: UtilsRespStatus & {
data?: ServiceEventCheckinQueryResponse;
};
};
export type GetEventCheckinQueryResponse = GetEventCheckinQueryResponses[keyof GetEventCheckinQueryResponses];
export type PostEventCheckinSubmitData = {
/**
* Checkin Code Data
*/
body: ServiceEventCheckinSubmitData;
path?: never;
query?: never;
url: '/event/checkin/submit';
};
export type PostEventCheckinSubmitErrors = {
/**
* Invalid Code or Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PostEventCheckinSubmitError = PostEventCheckinSubmitErrors[keyof PostEventCheckinSubmitErrors];
export type PostEventCheckinSubmitResponses = {
/**
* Attendance marked successfully
*/
200: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PostEventCheckinSubmitResponse = PostEventCheckinSubmitResponses[keyof PostEventCheckinSubmitResponses];
export type GetEventInfoData = {
body?: never;
path?: never;
query: {
/**
* Event UUID
*/
event_id: string;
};
url: '/event/info';
};
export type GetEventInfoErrors = {
/**
* Invalid Input
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Event Not Found
*/
404: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetEventInfoError = GetEventInfoErrors[keyof GetEventInfoErrors];
export type GetEventInfoResponses = {
/**
* Successful retrieval
*/
200: UtilsRespStatus & {
data?: DataEventIndexDoc;
};
};
export type GetEventInfoResponse = GetEventInfoResponses[keyof GetEventInfoResponses];
export type GetEventListData = {
body?: never;
path?: never;
query: {
/**
* Maximum number of events to return (default 20)
*/
limit?: string;
/**
* Number of events to skip
*/
offset: string;
};
url: '/event/list';
};
export type GetEventListErrors = {
/**
* Invalid Input (Missing offset or malformed parameters)
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error (Database query failed)
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetEventListError = GetEventListErrors[keyof GetEventListErrors];
export type GetEventListResponses = {
/**
* Successful paginated list retrieval
*/
200: UtilsRespStatus & {
data?: Array<DataEventIndexDoc>;
};
};
export type GetEventListResponse = GetEventListResponses[keyof GetEventListResponses];
export type GetUserInfoData = {
body?: never;
path?: never;
query?: never;
url: '/user/info';
};
export type GetUserInfoErrors = {
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* User Not Found
*/
404: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error (UUID Parse Failed)
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetUserInfoError = GetUserInfoErrors[keyof GetUserInfoErrors];
export type GetUserInfoResponses = {
/**
* Successful profile retrieval
*/
200: UtilsRespStatus & {
data?: ServiceUserUserInfoData;
};
};
export type GetUserInfoResponse = GetUserInfoResponses[keyof GetUserInfoResponses];
export type GetUserInfoByUserIdData = {
body?: never;
path: {
/**
* Other user id
*/
user_id: string;
};
query?: never;
url: '/user/info/{user_id}';
};
export type GetUserInfoByUserIdErrors = {
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* User Not Public
*/
403: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* User Not Found
*/
404: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error (UUID Parse Failed)
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetUserInfoByUserIdError = GetUserInfoByUserIdErrors[keyof GetUserInfoByUserIdErrors];
export type GetUserInfoByUserIdResponses = {
/**
* Successful profile retrieval
*/
200: UtilsRespStatus & {
data?: ServiceUserUserInfoData;
};
};
export type GetUserInfoByUserIdResponse = GetUserInfoByUserIdResponses[keyof GetUserInfoByUserIdResponses];
export type GetUserListData = {
body?: never;
path?: never;
query: {
/**
* Maximum number of users to return (default 0)
*/
limit?: string;
/**
* Number of users to skip
*/
offset: string;
};
url: '/user/list';
};
export type GetUserListErrors = {
/**
* Invalid Input (Format Error)
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error (Search Engine or Missing Offset)
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type GetUserListError = GetUserListErrors[keyof GetUserListErrors];
export type GetUserListResponses = {
/**
* Successful paginated list retrieval
*/
200: UtilsRespStatus & {
data?: Array<DataUserIndexDoc>;
};
};
export type GetUserListResponse = GetUserListResponses[keyof GetUserListResponses];
export type PatchUserUpdateData = {
/**
* Updated User Profile Data
*/
body: ServiceUserUserInfoData;
path?: never;
query?: never;
url: '/user/update';
};
export type PatchUserUpdateErrors = {
/**
* Invalid Input (Validation Failed)
*/
400: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Missing User ID / Unauthorized
*/
401: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
/**
* Internal Server Error (Database Error / UUID Parse Failed)
*/
500: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PatchUserUpdateError = PatchUserUpdateErrors[keyof PatchUserUpdateErrors];
export type PatchUserUpdateResponses = {
/**
* Successful profile update
*/
200: UtilsRespStatus & {
data?: {
[key: string]: unknown;
};
};
};
export type PatchUserUpdateResponse = PatchUserUpdateResponses[keyof PatchUserUpdateResponses];

View File

@@ -1,284 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { z } from 'zod';
export const zDataEventIndexDoc = z.object({
description: z.optional(z.string()),
end_time: z.optional(z.string()),
event_id: z.optional(z.string()),
name: z.optional(z.string()),
start_time: z.optional(z.string()),
thumbnail: z.optional(z.string()),
type: z.optional(z.string())
});
export const zDataUserIndexDoc = z.object({
avatar: z.optional(z.string()),
email: z.optional(z.string()),
nickname: z.optional(z.string()),
subtitle: z.optional(z.string()),
type: z.optional(z.string()),
user_id: z.optional(z.string()),
username: z.optional(z.string())
});
export const zServiceAuthExchangeData = z.object({
client_id: z.optional(z.string()),
redirect_uri: z.optional(z.string()),
state: z.optional(z.string())
});
export const zServiceAuthExchangeResponse = z.object({
redirect_uri: z.optional(z.string())
});
export const zServiceAuthMagicData = z.object({
client_id: z.optional(z.string()),
client_ip: z.optional(z.string()),
email: z.optional(z.string()),
redirect_uri: z.optional(z.string()),
state: z.optional(z.string()),
turnstile_token: z.optional(z.string())
});
export const zServiceAuthMagicResponse = z.object({
uri: z.optional(z.string())
});
export const zServiceAuthRefreshData = z.object({
refresh_token: z.optional(z.string())
});
export const zServiceAuthTokenData = z.object({
code: z.optional(z.string())
});
export const zServiceAuthTokenResponse = z.object({
access_token: z.optional(z.string()),
refresh_token: z.optional(z.string())
});
export const zServiceEventCheckinQueryResponse = z.object({
checkin_at: z.optional(z.string())
});
export const zServiceEventCheckinResponse = z.object({
checkin_code: z.optional(z.string())
});
export const zServiceEventCheckinSubmitData = z.object({
checkin_code: z.optional(z.string())
});
export const zServiceUserUserInfoData = z.object({
allow_public: z.optional(z.boolean()),
avatar: z.optional(z.string()),
bio: z.optional(z.string()),
email: z.optional(z.string()),
nickname: z.optional(z.string()),
permission_level: z.optional(z.int()),
subtitle: z.optional(z.string()),
user_id: z.optional(z.string()),
username: z.optional(z.string())
});
export const zUtilsRespStatus = z.object({
code: z.optional(z.int()),
data: z.optional(z.unknown()),
error_id: z.optional(z.string()),
status: z.optional(z.string())
});
export const zPostAuthExchangeData = z.object({
body: zServiceAuthExchangeData,
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Successful exchange
*/
export const zPostAuthExchangeResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthExchangeResponse)
}));
export const zPostAuthMagicData = z.object({
body: zServiceAuthMagicData,
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Successful request
*/
export const zPostAuthMagicResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthMagicResponse)
}));
export const zGetAuthRedirectData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.object({
client_id: z.string(),
redirect_uri: z.string(),
code: z.string(),
state: z.optional(z.string())
})
});
export const zPostAuthRefreshData = z.object({
body: zServiceAuthRefreshData,
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Successful rotation
*/
export const zPostAuthRefreshResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthTokenResponse)
}));
export const zPostAuthTokenData = z.object({
body: zServiceAuthTokenData,
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Successful token issuance
*/
export const zPostAuthTokenResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceAuthTokenResponse)
}));
export const zGetEventCheckinData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.object({
event_id: z.string()
})
});
/**
* Successfully generated code
*/
export const zGetEventCheckinResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceEventCheckinResponse)
}));
export const zGetEventCheckinQueryData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.object({
event_id: z.string()
})
});
/**
* Current attendance status
*/
export const zGetEventCheckinQueryResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceEventCheckinQueryResponse)
}));
export const zPostEventCheckinSubmitData = z.object({
body: zServiceEventCheckinSubmitData,
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Attendance marked successfully
*/
export const zPostEventCheckinSubmitResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.record(z.string(), z.unknown()))
}));
export const zGetEventInfoData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.object({
event_id: z.string()
})
});
/**
* Successful retrieval
*/
export const zGetEventInfoResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zDataEventIndexDoc)
}));
export const zGetEventListData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.object({
limit: z.optional(z.string()),
offset: z.string()
})
});
/**
* Successful paginated list retrieval
*/
export const zGetEventListResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.array(zDataEventIndexDoc))
}));
export const zGetUserInfoData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Successful profile retrieval
*/
export const zGetUserInfoResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceUserUserInfoData)
}));
export const zGetUserInfoByUserIdData = z.object({
body: z.optional(z.never()),
path: z.object({
user_id: z.string()
}),
query: z.optional(z.never())
});
/**
* Successful profile retrieval
*/
export const zGetUserInfoByUserIdResponse = zUtilsRespStatus.and(z.object({
data: z.optional(zServiceUserUserInfoData)
}));
export const zGetUserListData = z.object({
body: z.optional(z.never()),
path: z.optional(z.never()),
query: z.object({
limit: z.optional(z.string()),
offset: z.string()
})
});
/**
* Successful paginated list retrieval
*/
export const zGetUserListResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.array(zDataUserIndexDoc))
}));
export const zPatchUserUpdateData = z.object({
body: zServiceUserUserInfoData,
path: z.optional(z.never()),
query: z.optional(z.never())
});
/**
* Successful profile update
*/
export const zPatchUserUpdateResponse = zUtilsRespStatus.and(z.object({
data: z.optional(z.record(z.string(), z.unknown()))
}));

View File

@@ -9,6 +9,7 @@ import {
DialogTrigger,
} from '@/components/ui/dialog';
import { QRCode } from '@/components/ui/shadcn-io/qr-code';
import { useCheckinCode } from '@/hooks/data/useGetCheckInCode';
import { Button } from '../ui/button';
export function QrDialog(
@@ -34,8 +35,7 @@ export function QrDialog(
}
function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) {
// const { data } = useCheckinCode(eventId, enabled);
const data = { data: { checkin_code: `dummy${eventId}${enabled}` } };
const { data } = useCheckinCode(eventId, enabled);
return data
? (
<>

View File

@@ -32,7 +32,7 @@ export function LoginForm({
event.preventDefault();
const formData = new FormData(formRef.current!);
const email = formData.get('email')! as string;
mutateAsync({ body: { email, turnstile_token: token!, ...oauthParams } }).then(() => {
mutateAsync({ email, turnstile_token: token!, ...oauthParams }).then(() => {
void navigate({ to: '/magicLinkSent', search: { email } });
}).catch((error) => {
console.error(error);

View File

@@ -1,6 +1,4 @@
import type { ServiceUserUserInfoData } from '@/client';
import { useForm } from '@tanstack/react-form';
import { useState } from 'react';
import { toast } from 'sonner';
import z from 'zod';
import { Button } from '@/components/ui/button';
@@ -22,16 +20,16 @@ import {
Input,
} from '@/components/ui/input';
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { Switch } from '../ui/switch';
import { useUserInfo } from '@/hooks/data/useUserInfo';
const formSchema = z.object({
username: z.string().min(5),
nickname: z.string(),
subtitle: z.string(),
avatar: z.url().or(z.literal('')),
allow_public: z.boolean(),
nickname: z.string().min(1),
subtitle: z.string().min(1),
avatar: z.url().min(1),
});
export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
export function EditProfileDialog() {
const { data: user } = useUserInfo();
const { mutateAsync } = useUpdateUser();
const form = useForm({
@@ -40,7 +38,6 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
username: user.username,
nickname: user.nickname,
subtitle: user.subtitle,
allow_public: user.allow_public,
},
validators: {
onBlur: formSchema,
@@ -49,7 +46,7 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
value,
}) => {
try {
await mutateAsync({ body: value });
await mutateAsync(value);
toast.success('个人资料更新成功');
}
catch (error) {
@@ -59,16 +56,8 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
},
});
const [open, setOpen] = useState(false);
if (!open) {
setTimeout(() => {
form.reset();
}, 200);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="w-full" size="lg"></Button>
</DialogTrigger>
@@ -77,7 +66,7 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit().then(() => setOpen(false));
void form.handleSubmit();
}}
className="grid gap-4"
>
@@ -148,24 +137,13 @@ export function EditProfileDialog({ user }: { user: ServiceUserUserInfoData }) {
</Field>
)}
</form.Field>
<form.Field name="allow_public">
{field => (
<Field orientation="horizontal" className="my-2">
<FieldLabel htmlFor="allow_public"></FieldLabel>
<Switch id="allow_public" onCheckedChange={e => field.handleChange(e)} defaultChecked={user.allow_public} />
</Field>
)}
</form.Field>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline"></Button>
</DialogClose>
<form.Subscribe
selector={state => [state.canSubmit]}
children={([canSubmit]) => (
<Button type="submit" disabled={!canSubmit}></Button>
)}
/>
<DialogClose asChild>
<Button type="submit"></Button>
</DialogClose>
</DialogFooter>
</form>
</DialogContent>

View File

@@ -1,34 +1,22 @@
import type { ServiceUserUserInfoData } from '@/client';
import { identicon } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import MDEditor from '@uiw/react-md-editor';
import {
isEmpty,
isNil,
} from 'lodash-es';
import { isNil } from 'lodash-es';
import { Mail, Pencil } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import Markdown from 'react-markdown';
import { toast } from 'sonner';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
import { Button } from '../ui/button';
import { EditProfileDialog } from './edit-profile-dialog';
export function Profile({ user }: { user: ServiceUserUserInfoData }) {
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio ?? ''));
export function MainProfile() {
const { data: user } = useUserInfo();
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio));
const [enableBioEdit, setEnableBioEdit] = useState(false);
const { mutateAsync } = useUpdateUser();
const IdentIcon = useMemo(() => {
const avatar = createAvatar(identicon, {
size: 128,
seed: user.user_id,
}).toDataUri();
return <img src={avatar} alt="Avatar" />;
}, [user.user_id]);
return (
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
@@ -36,7 +24,8 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) {
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Avatar className="size-16 rounded-full border-2 border-muted lg:size-64">
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
<AvatarImage src={user.avatar} alt={user.nickname} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="flex flex-1 flex-col justify-center lg:mt-3">
<span className="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span>
@@ -48,7 +37,7 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) {
{user.email}
</div>
</div>
<EditProfileDialog user={user} />
<EditProfileDialog />
</div>
</div>
<section className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
@@ -72,7 +61,7 @@ export function Profile({ user }: { user: ServiceUserUserInfoData }) {
else {
if (!isNil(bio)) {
try {
await mutateAsync({ body: { bio: utf8ToBase64(bio) } });
await mutateAsync({ bio: utf8ToBase64(bio) });
setEnableBioEdit(false);
}
catch (error) {

View File

@@ -1,30 +0,0 @@
import { Mail } from 'lucide-react';
import { Skeleton } from '../ui/skeleton';
export function ProfileError({ reason }: { reason: string }) {
return (
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Skeleton className="size-16 rounded-full border-2 border-muted lg:size-64" />
<div className="flex flex-1 flex-col justify-center lg:mt-3 gap-2">
<Skeleton className="w-32 h-8" />
<Skeleton className="w-20 h-6" />
</div>
</div>
<div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
<Mail className="h-4 w-4 stroke-muted-foreground" />
<Skeleton className="w-32 h-4" />
</div>
</div>
<Skeleton className="w-64 h-[40px]" />
</div>
</div>
<Skeleton className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center flex items-center justify-center">
{reason}
</Skeleton>
</div>
);
}

View File

@@ -1,29 +0,0 @@
import { Mail } from 'lucide-react';
import { Skeleton } from '../ui/skeleton';
export function ProfileSkeleton() {
return (
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-row gap-3 w-full lg:flex-col">
<Skeleton className="size-16 rounded-full border-2 border-muted lg:size-64" />
<div className="flex flex-1 flex-col justify-center lg:mt-3 gap-2">
<Skeleton className="w-32 h-8" />
<Skeleton className="w-20 h-6" />
</div>
</div>
<div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
<Mail className="h-4 w-4 stroke-muted-foreground" />
<Skeleton className="w-32 h-4" />
</div>
</div>
<Skeleton className="w-64 h-[40px]" />
</div>
</div>
<Skeleton className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
</Skeleton>
</div>
);
}

View File

@@ -1,14 +1,11 @@
import { identicon } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import {
IconDotsVertical,
IconLogout,
} from '@tabler/icons-react';
import { isEmpty } from 'lodash-es';
import { useMemo } from 'react';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/components/ui/avatar';
import {
@@ -32,18 +29,9 @@ import { Skeleton } from '../ui/skeleton';
function NavUser_() {
const { isMobile } = useSidebar();
const { data } = useUserInfo();
const user = data.data!;
const { data: user } = useUserInfo();
const { logout } = useLogout();
const IdentIcon = useMemo(() => {
const avatar = createAvatar(identicon, {
size: 128,
seed: user.user_id,
}).toDataUri();
return <img src={avatar} alt="Avatar" />;
}, [user.user_id]);
return (
<SidebarMenu>
<SidebarMenuItem>
@@ -53,8 +41,9 @@ function NavUser_() {
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
<Avatar className="h-8 w-8 rounded-lg grayscale">
<AvatarImage src={user.avatar} alt={user.nickname} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}</span>
@@ -74,7 +63,8 @@ function NavUser_() {
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
<AvatarImage src={user.avatar} alt={user.nickname} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}</span>

View File

@@ -36,6 +36,7 @@ export function ThemeProvider({
root.classList.add(theme);
}, [theme]);
// eslint-disable-next-line react/no-unstable-context-value
const value = {
theme,
setTheme: (theme: Theme) => {

View File

@@ -1,310 +0,0 @@
"use client"
import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
const Combobox = ComboboxPrimitive.Root
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="combobox-trigger-icon"
className="text-muted-foreground pointer-events-none size-4"
/>
</ComboboxPrimitive.Trigger>
)
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
render={<InputGroupButton variant="ghost" size="icon-xs" />}
className={cn(className)}
{...props}
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.Clear>
)
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean
showClear?: boolean
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
asChild
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
>
<ComboboxTrigger />
</InputGroupButton>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
)
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 group/combobox-content relative max-h-96 w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-md shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none",
className
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
)
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0",
className
)}
{...props}
/>
)
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
data-slot="combobox-item-indicator"
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
)
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
)
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn(
"text-muted-foreground px-2 py-1.5 text-xs pointer-coarse:px-3 pointer-coarse:py-2 pointer-coarse:text-sm",
className
)}
{...props}
/>
)
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
)
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex",
className
)}
{...props}
/>
)
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-[3px] has-aria-invalid:ring-[3px] has-data-[slot=combobox-chip]:px-1.5",
className
)}
{...props}
/>
)
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"bg-muted text-foreground flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
className
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-sm" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
)
}
function ComboboxChipsInput({
className,
children,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
)
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null)
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
}

View File

@@ -1,168 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}

View File

@@ -1,33 +0,0 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -1,18 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,39 +0,0 @@
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function EventCard({ type, coverImage, eventName, description, startTime, endTime }:
{
type: 'official' | 'party',
coverImage: string, eventName: string, description: string, startTime: string, endTime: string
}) {
return (
<Card className="relative mx-auto w-full max-w-sm pt-0">
<div className="absolute inset-0 z-30 aspect-video bg-black/35" />
<img
src="https://avatar.vercel.sh/shadcn1"
alt="Event cover"
className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale dark:brightness-40"
/>
<CardHeader>
<CardAction>
<Badge variant="secondary">Featured</Badge>
</CardAction>
<CardTitle>Design systems meetup</CardTitle>
<CardDescription>
A practical talk on component APIs, accessibility, and shipping
faster.
</CardDescription>
</CardHeader>
<CardFooter>
<Button className="w-full">View Event</Button>
</CardFooter>
</Card>
)
}

View File

@@ -1,16 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner';
import { postAuthExchangeMutation } from '@/client/@tanstack/react-query.gen';
export function useExchangeToken() {
return useMutation({
...postAuthExchangeMutation(),
onSuccess: (data) => {
window.location.href = data.data!.redirect_uri!;
},
onError: (error) => {
console.error(error);
toast('An error occurred while exchanging the token. Please login manually.');
},
});
}

View File

@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { axiosClient } from '@/lib/axios';
export function useCheckinCode(eventId: string, enabled: boolean) {
return useQuery({
queryKey: ['getCheckinCode', eventId],
queryFn: async () => {
return axiosClient.get<{
checkin_code: string;
}>('/user/checkin', {
params: {
event_id: eventId,
},
});
},
enabled,
});
}

View File

@@ -1,8 +1,16 @@
import type { AuthorizeSearchParams } from '@/routes/authorize';
import { useMutation } from '@tanstack/react-query';
import { postAuthMagicMutation } from '@/client/@tanstack/react-query.gen';
import { axiosClient } from '@/lib/axios';
interface GetMagicLinkPayload extends AuthorizeSearchParams {
email: string;
turnstile_token: string;
}
export function useGetMagicLink() {
return useMutation({
...postAuthMagicMutation(),
mutationFn: async (payload: GetMagicLinkPayload) => {
return axiosClient.post<{ status: string }>('/auth/magic', payload);
},
});
}

View File

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

View File

@@ -1,20 +1,23 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import {
getUserInfoByUserIdOptions,
getUserInfoOptions,
} from '@/client/@tanstack/react-query.gen';
import { axiosClient } from '@/lib/axios';
export function useUserInfo() {
return useSuspenseQuery({
...getUserInfoOptions(),
queryKey: ['userInfo'],
queryFn: async () => {
const response = await axiosClient.get<{
username: string;
user_id: string;
email: string;
type: string;
nickname: string;
subtitle: string;
avatar: string;
bio: string;
}
>('/user/info');
return response.data;
},
staleTime: 10 * 60 * 1000,
});
}
export function useOtherUserInfo(userId: string) {
return useSuspenseQuery({
...getUserInfoByUserIdOptions({ path: { user_id: userId } }),
staleTime: 10 * 60 * 1000,
retry: (_failureCount, error) => error.code !== 403,
});
}

View File

@@ -0,0 +1,11 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { axiosClient } from '@/lib/axios';
export function useValidateMagicLink(ticket: string) {
return useSuspenseQuery({
queryKey: ['validateMagicLink', ticket],
queryFn: async () => {
return axiosClient.get<{ access_token: string; refresh_token: string }>('/auth/magic/verify', { params: { token: ticket } });
},
});
}

View File

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

View File

@@ -1,63 +0,0 @@
import { isEmpty, isNil } from 'lodash-es';
import { client } from '@/client/client.gen';
import {
doRefreshToken,
getAccessToken,
getRefreshToken,
logout,
setAccessToken,
setRefreshToken,
} from './token';
export function configInternalApiClient() {
client.setConfig({
baseUrl: '/api/v1/',
headers: {
'X-Api-Version': 'latest',
},
});
client.interceptors.request.use((request) => {
const token = getAccessToken();
if (!isNil(token) && !isEmpty(token)) {
request.headers.set('Authorization', `Bearer ${token}`);
}
return request;
});
client.interceptors.response.use(async (response, request, options) => {
if (response.status === 401) {
// Avoid infinite loop if the refresh token request itself fails
if (request.url.includes('/auth/refresh')) {
// Refresh token failed, clear tokens and redirect to login page
logout('Session expired');
}
else {
const refreshToken = getRefreshToken();
if (isNil(refreshToken) || isEmpty(refreshToken)) {
logout('You are not logged in');
}
else {
const refreshResponse = await doRefreshToken(refreshToken);
if (!isEmpty(refreshResponse)) {
const { access_token, refresh_token } = refreshResponse;
setAccessToken(access_token!);
setRefreshToken(refresh_token!);
const fetchFn = options.fetch ?? globalThis.fetch;
const headers = new Headers(request.headers);
headers.set('Authorization', `Bearer ${access_token}`);
return fetchFn(request.url, {
method: request.method,
headers,
body: (options.serializedBody ?? options.body) as BodyInit | null | undefined,
signal: request.signal,
});
}
}
}
}
return response;
});
}

View File

@@ -1,5 +1,4 @@
import {
IconCalendarEvent,
IconDashboard,
IconUser,
} from '@tabler/icons-react';
@@ -11,11 +10,6 @@ export const navData = {
url: '/',
icon: IconDashboard,
},
{
title: '活动列表',
url: '/events',
icon: IconCalendarEvent,
},
],
navSecondary: [
{

View File

@@ -1,52 +1,46 @@
import type { ServiceAuthTokenResponse } from '@/client';
import { toast } from 'sonner';
import { postAuthRefresh } from '@/client';
import { router } from './router';
import { axiosClient, HEADER_API_VERSION } from './axios';
const ACCESS_TOKEN_LOCALSTORAGE_KEY = 'token';
const REFRESH_TOKEN_LOCALSTORAGE_KEY = 'refreshToken';
export function setAccessToken(token: string) {
localStorage.setItem(ACCESS_TOKEN_LOCALSTORAGE_KEY, token);
export function setToken(token: string) {
localStorage.setItem('token', token);
}
export function getAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_LOCALSTORAGE_KEY);
export function getToken() {
return localStorage.getItem('token');
}
export function removeAccessToken() {
localStorage.removeItem(ACCESS_TOKEN_LOCALSTORAGE_KEY);
export function removeToken() {
localStorage.removeItem('token');
}
export function hasToken() {
return getToken() !== null;
}
export function setRefreshToken(refreshToken: string) {
localStorage.setItem(REFRESH_TOKEN_LOCALSTORAGE_KEY, refreshToken);
localStorage.setItem('refreshToken', refreshToken);
}
export function getRefreshToken() {
return localStorage.getItem(REFRESH_TOKEN_LOCALSTORAGE_KEY);
}
export function removeRefreshToken() {
localStorage.removeItem(REFRESH_TOKEN_LOCALSTORAGE_KEY);
return localStorage.getItem('refreshToken');
}
export function clearTokens() {
removeAccessToken();
removeRefreshToken();
removeToken();
setRefreshToken('');
}
export async function doRefreshToken(refreshToken: string): Promise<ServiceAuthTokenResponse | undefined> {
const { data } = await postAuthRefresh({
body: {
refresh_token: refreshToken,
},
export async function doSetTokenByCode(code: string) {
return new Promise<void>((resolve, reject) => {
axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION }).then(({ data }) => {
setToken(data.access_token);
setRefreshToken(data.refresh_token);
resolve();
}).catch((error) => {
reject(error);
});
});
return data?.data;
}
export function logout(message: string = 'Logged out') {
clearTokens();
void router.navigate({ to: '/authorize' }).then(() => {
toast.error(message);
});
export async function doRefreshToken() {
return axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token: getRefreshToken() }, { headers: HEADER_API_VERSION });
}

View File

@@ -2,9 +2,6 @@ import { RouterProvider } from '@tanstack/react-router';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { router } from '@/lib/router';
import { configInternalApiClient } from './lib/client';
configInternalApiClient();
// Render the app
const rootElement = document.getElementById('root')!;

View File

@@ -12,11 +12,9 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as TokenRouteImport } from './routes/token'
import { Route as MagicLinkSentRouteImport } from './routes/magicLinkSent'
import { Route as AuthorizeRouteImport } from './routes/authorize'
import { Route as WorkbenchLayoutRouteImport } from './routes/_workbenchLayout'
import { Route as WorkbenchLayoutIndexRouteImport } from './routes/_workbenchLayout/index'
import { Route as WorkbenchLayoutEventsRouteImport } from './routes/_workbenchLayout/events'
import { Route as WorkbenchLayoutProfileIndexRouteImport } from './routes/_workbenchLayout/profile.index'
import { Route as WorkbenchLayoutProfileUserIdRouteImport } from './routes/_workbenchLayout/profile.$userId'
import { Route as SidebarLayoutRouteImport } from './routes/_sidebarLayout'
import { Route as SidebarLayoutIndexRouteImport } from './routes/_sidebarLayout/index'
import { Route as SidebarLayoutProfileRouteImport } from './routes/_sidebarLayout/profile'
const TokenRoute = TokenRouteImport.update({
id: '/token',
@@ -33,95 +31,61 @@ const AuthorizeRoute = AuthorizeRouteImport.update({
path: '/authorize',
getParentRoute: () => rootRouteImport,
} as any)
const WorkbenchLayoutRoute = WorkbenchLayoutRouteImport.update({
id: '/_workbenchLayout',
const SidebarLayoutRoute = SidebarLayoutRouteImport.update({
id: '/_sidebarLayout',
getParentRoute: () => rootRouteImport,
} as any)
const WorkbenchLayoutIndexRoute = WorkbenchLayoutIndexRouteImport.update({
const SidebarLayoutIndexRoute = SidebarLayoutIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => WorkbenchLayoutRoute,
getParentRoute: () => SidebarLayoutRoute,
} as any)
const WorkbenchLayoutEventsRoute = WorkbenchLayoutEventsRouteImport.update({
id: '/events',
path: '/events',
getParentRoute: () => WorkbenchLayoutRoute,
const SidebarLayoutProfileRoute = SidebarLayoutProfileRouteImport.update({
id: '/profile',
path: '/profile',
getParentRoute: () => SidebarLayoutRoute,
} as any)
const WorkbenchLayoutProfileIndexRoute =
WorkbenchLayoutProfileIndexRouteImport.update({
id: '/profile/',
path: '/profile/',
getParentRoute: () => WorkbenchLayoutRoute,
} as any)
const WorkbenchLayoutProfileUserIdRoute =
WorkbenchLayoutProfileUserIdRouteImport.update({
id: '/profile/$userId',
path: '/profile/$userId',
getParentRoute: () => WorkbenchLayoutRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof WorkbenchLayoutIndexRoute
'/': typeof SidebarLayoutIndexRoute
'/authorize': typeof AuthorizeRoute
'/magicLinkSent': typeof MagicLinkSentRoute
'/token': typeof TokenRoute
'/events': typeof WorkbenchLayoutEventsRoute
'/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute
'/profile/': typeof WorkbenchLayoutProfileIndexRoute
'/profile': typeof SidebarLayoutProfileRoute
}
export interface FileRoutesByTo {
'/authorize': typeof AuthorizeRoute
'/magicLinkSent': typeof MagicLinkSentRoute
'/token': typeof TokenRoute
'/events': typeof WorkbenchLayoutEventsRoute
'/': typeof WorkbenchLayoutIndexRoute
'/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute
'/profile': typeof WorkbenchLayoutProfileIndexRoute
'/profile': typeof SidebarLayoutProfileRoute
'/': typeof SidebarLayoutIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_workbenchLayout': typeof WorkbenchLayoutRouteWithChildren
'/_sidebarLayout': typeof SidebarLayoutRouteWithChildren
'/authorize': typeof AuthorizeRoute
'/magicLinkSent': typeof MagicLinkSentRoute
'/token': typeof TokenRoute
'/_workbenchLayout/events': typeof WorkbenchLayoutEventsRoute
'/_workbenchLayout/': typeof WorkbenchLayoutIndexRoute
'/_workbenchLayout/profile/$userId': typeof WorkbenchLayoutProfileUserIdRoute
'/_workbenchLayout/profile/': typeof WorkbenchLayoutProfileIndexRoute
'/_sidebarLayout/profile': typeof SidebarLayoutProfileRoute
'/_sidebarLayout/': typeof SidebarLayoutIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/authorize'
| '/magicLinkSent'
| '/token'
| '/events'
| '/profile/$userId'
| '/profile/'
fullPaths: '/' | '/authorize' | '/magicLinkSent' | '/token' | '/profile'
fileRoutesByTo: FileRoutesByTo
to:
| '/authorize'
| '/magicLinkSent'
| '/token'
| '/events'
| '/'
| '/profile/$userId'
| '/profile'
to: '/authorize' | '/magicLinkSent' | '/token' | '/profile' | '/'
id:
| '__root__'
| '/_workbenchLayout'
| '/_sidebarLayout'
| '/authorize'
| '/magicLinkSent'
| '/token'
| '/_workbenchLayout/events'
| '/_workbenchLayout/'
| '/_workbenchLayout/profile/$userId'
| '/_workbenchLayout/profile/'
| '/_sidebarLayout/profile'
| '/_sidebarLayout/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
WorkbenchLayoutRoute: typeof WorkbenchLayoutRouteWithChildren
SidebarLayoutRoute: typeof SidebarLayoutRouteWithChildren
AuthorizeRoute: typeof AuthorizeRoute
MagicLinkSentRoute: typeof MagicLinkSentRoute
TokenRoute: typeof TokenRoute
@@ -150,64 +114,46 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthorizeRouteImport
parentRoute: typeof rootRouteImport
}
'/_workbenchLayout': {
id: '/_workbenchLayout'
'/_sidebarLayout': {
id: '/_sidebarLayout'
path: ''
fullPath: '/'
preLoaderRoute: typeof WorkbenchLayoutRouteImport
preLoaderRoute: typeof SidebarLayoutRouteImport
parentRoute: typeof rootRouteImport
}
'/_workbenchLayout/': {
id: '/_workbenchLayout/'
'/_sidebarLayout/': {
id: '/_sidebarLayout/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof WorkbenchLayoutIndexRouteImport
parentRoute: typeof WorkbenchLayoutRoute
preLoaderRoute: typeof SidebarLayoutIndexRouteImport
parentRoute: typeof SidebarLayoutRoute
}
'/_workbenchLayout/events': {
id: '/_workbenchLayout/events'
path: '/events'
fullPath: '/events'
preLoaderRoute: typeof WorkbenchLayoutEventsRouteImport
parentRoute: typeof WorkbenchLayoutRoute
}
'/_workbenchLayout/profile/': {
id: '/_workbenchLayout/profile/'
'/_sidebarLayout/profile': {
id: '/_sidebarLayout/profile'
path: '/profile'
fullPath: '/profile/'
preLoaderRoute: typeof WorkbenchLayoutProfileIndexRouteImport
parentRoute: typeof WorkbenchLayoutRoute
}
'/_workbenchLayout/profile/$userId': {
id: '/_workbenchLayout/profile/$userId'
path: '/profile/$userId'
fullPath: '/profile/$userId'
preLoaderRoute: typeof WorkbenchLayoutProfileUserIdRouteImport
parentRoute: typeof WorkbenchLayoutRoute
fullPath: '/profile'
preLoaderRoute: typeof SidebarLayoutProfileRouteImport
parentRoute: typeof SidebarLayoutRoute
}
}
}
interface WorkbenchLayoutRouteChildren {
WorkbenchLayoutEventsRoute: typeof WorkbenchLayoutEventsRoute
WorkbenchLayoutIndexRoute: typeof WorkbenchLayoutIndexRoute
WorkbenchLayoutProfileUserIdRoute: typeof WorkbenchLayoutProfileUserIdRoute
WorkbenchLayoutProfileIndexRoute: typeof WorkbenchLayoutProfileIndexRoute
interface SidebarLayoutRouteChildren {
SidebarLayoutProfileRoute: typeof SidebarLayoutProfileRoute
SidebarLayoutIndexRoute: typeof SidebarLayoutIndexRoute
}
const WorkbenchLayoutRouteChildren: WorkbenchLayoutRouteChildren = {
WorkbenchLayoutEventsRoute: WorkbenchLayoutEventsRoute,
WorkbenchLayoutIndexRoute: WorkbenchLayoutIndexRoute,
WorkbenchLayoutProfileUserIdRoute: WorkbenchLayoutProfileUserIdRoute,
WorkbenchLayoutProfileIndexRoute: WorkbenchLayoutProfileIndexRoute,
const SidebarLayoutRouteChildren: SidebarLayoutRouteChildren = {
SidebarLayoutProfileRoute: SidebarLayoutProfileRoute,
SidebarLayoutIndexRoute: SidebarLayoutIndexRoute,
}
const WorkbenchLayoutRouteWithChildren = WorkbenchLayoutRoute._addFileChildren(
WorkbenchLayoutRouteChildren,
const SidebarLayoutRouteWithChildren = SidebarLayoutRoute._addFileChildren(
SidebarLayoutRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
WorkbenchLayoutRoute: WorkbenchLayoutRouteWithChildren,
SidebarLayoutRoute: SidebarLayoutRouteWithChildren,
AuthorizeRoute: AuthorizeRoute,
MagicLinkSentRoute: MagicLinkSentRoute,
TokenRoute: TokenRoute,

View File

@@ -3,7 +3,7 @@ import { AppSidebar } from '@/components/sidebar/app-sidebar';
import { SiteHeader } from '@/components/site-header';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
export const Route = createFileRoute('/_workbenchLayout')({
export const Route = createFileRoute('/_sidebarLayout')({
component: RouteComponent,
});

View File

@@ -1,7 +1,15 @@
import { createFileRoute } from '@tanstack/react-router';
import { createFileRoute, redirect } from '@tanstack/react-router';
import { hasToken } from '@/lib/token';
export const Route = createFileRoute('/_workbenchLayout/')({
export const Route = createFileRoute('/_sidebarLayout/')({
component: Index,
loader: async () => {
if (!hasToken()) {
throw redirect({
to: '/authorize',
});
}
},
});
function Index() {

View File

@@ -0,0 +1,14 @@
import { createFileRoute } from '@tanstack/react-router';
import { MainProfile } from '@/components/profile/main-profile';
export const Route = createFileRoute('/_sidebarLayout/profile')({
component: RouteComponent,
});
function RouteComponent() {
return (
<div className="flex h-full flex-col gap-6 px-4 py-6">
<MainProfile />
</div>
);
}

View File

@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/_workbenchLayout/events')({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/_sidebarLayout/events"!</div>;
}

View File

@@ -1,34 +0,0 @@
import { createFileRoute } from '@tanstack/react-router';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Profile } from '@/components/profile/profile';
import { ProfileError } from '@/components/profile/profile.error';
import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
import { useOtherUserInfo } from '@/hooks/data/useUserInfo';
export const Route = createFileRoute('/_workbenchLayout/profile/$userId')({
component: RouteComponent,
});
function ProfileByUserId({ userId }: { userId: string }) {
const { data } = useOtherUserInfo(userId);
return <Profile user={data.data!} />;
}
function RouteComponent() {
const { userId } = Route.useParams();
return (
<div className="flex h-full flex-col gap-6 px-4 py-6">
<ErrorBoundary fallbackRender={(error) => {
if ((error.error as { code: number }).code === 403)
return <ProfileError reason="用户个人资料未公开" />;
else return <ProfileError reason="获取用户个人资料失败" />;
}}
>
<Suspense fallback={<ProfileSkeleton />}>
<ProfileByUserId userId={userId} />
</Suspense>
</ErrorBoundary>
</div>
);
}

View File

@@ -1,13 +0,0 @@
import { createFileRoute, Navigate } from '@tanstack/react-router';
import { useUserInfo } from '@/hooks/data/useUserInfo';
export const Route = createFileRoute('/_workbenchLayout/profile/')({
component: RouteComponent,
});
function RouteComponent() {
const { data } = useUserInfo();
return (
<Navigate to="/profile/$userId" params={{ userId: data.data!.user_id! }} />
);
}

View File

@@ -1,19 +1,16 @@
import { createFileRoute } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter';
import { isNil } from 'lodash-es';
import { useEffect } from 'react';
import z from 'zod';
import { LoginForm } from '@/components/login-form';
import { useExchangeToken } from '@/hooks/data/useExchangeToken';
import { axiosClient } from '@/lib/axios';
import { generateOAuthState } from '@/lib/random';
import { getAccessToken } from '@/lib/token';
const baseUrl = import.meta.env.VITE_APP_BASE_URL;
import { getToken } from '@/lib/token';
const authorizeSchema = z.object({
response_type: z.literal('code').default('code'),
client_id: z.literal('org_client').default('org_client'),
redirect_uri: z.string().default(`${new URL(baseUrl).toString()}token`),
redirect_uri: z.string().default(`${new URL(import.meta.env.VITE_APP_BASE_URL as string).toString()}token`),
state: z.string().default(generateOAuthState()),
});
@@ -25,23 +22,24 @@ export const Route = createFileRoute('/authorize')({
});
function RouteComponent() {
const token = getAccessToken();
const token = getToken();
const oauthParams = Route.useSearch();
const mutation = useExchangeToken();
/**
* Auth by Token Flow
*/
useEffect(() => {
if (!isNil(token) && mutation.isIdle) {
mutation.mutate({
body: {
client_id: oauthParams.client_id,
redirect_uri: oauthParams.redirect_uri,
state: oauthParams.state,
},
});
}
}, [token, mutation.isIdle]);
if (!isNil(token)) {
axiosClient.post<{ redirect_uri: string }>('/auth/exchange', {
client_id: oauthParams.client_id,
redirect_uri: oauthParams.redirect_uri,
state: oauthParams.state,
}).then((res) => {
window.location.href = res.data.redirect_uri;
}).catch((e) => {
console.error(e);
return 'Token exchange failed';
});
return 'Redirecting';
}
return (
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="w-full max-w-sm">

View File

@@ -1,12 +1,7 @@
import { useMutation } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
useEffect,
useState,
} from 'react';
import { useEffect, useState } from 'react';
import z from 'zod';
import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen';
import { setAccessToken, setRefreshToken } from '@/lib/token';
import { doSetTokenByCode } from '@/lib/token';
const tokenCodeSchema = z.object({
code: z.string().nonempty(),
@@ -22,22 +17,13 @@ function RouteComponent() {
const [status, setStatus] = useState('Loading...');
const navigate = useNavigate();
const mutation = useMutation({
...postAuthTokenMutation(),
onSuccess: (data) => {
setAccessToken(data.data!.access_token!);
setRefreshToken(data.data!.refresh_token!);
void navigate({ to: '/' });
},
onError: () => {
setStatus('Error getting token');
},
});
useEffect(() => {
if (mutation.isIdle) {
mutation.mutate({ body: { code } });
}
doSetTokenByCode(code).then(() => {
void navigate({ to: '/' });
}).catch((_) => {
setStatus('Error getting token');
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div>{status}</div>;

View File

@@ -1,12 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { EventCard } from '@/components/workbenchCards/event-card';
const meta = {
title: 'Cards/EventCard',
component: EventCard,
} satisfies Meta<typeof EventCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};

View File

@@ -1,51 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Profile } from '@/components/profile/profile';
import { ProfileError } from '@/components/profile/profile.error';
import { ProfileSkeleton } from '@/components/profile/profile.skeleton';
const queryClient = new QueryClient();
const meta = {
title: 'Profile',
component: Profile,
decorators: [
Story => (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
),
],
} satisfies Meta<typeof Profile>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
user: {
username: 'nvirellia',
nickname: 'Noa Virellia',
subtitle: '天生骄傲',
email: 'noa@requiem.garden',
bio: '',
avatar: 'https://avatars.githubusercontent.com/u/54884471?v=4',
},
},
};
export const Skeleton: Story = {
render: () => <ProfileSkeleton />,
args: {
user: {},
},
};
export const Error: Story = {
render: () => <ProfileError reason="User profile is not public" />,
args: {
user: {
allow_public: false,
},
},
};

View File

@@ -1,11 +0,0 @@
interface ViteTypeOptions {
strictImportMetaEnv: unknown;
}
interface ImportMetaEnv {
readonly VITE_APP_BASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,4 +1,3 @@
/// <reference types="vitest/config" />
import path from 'node:path';
import tailwindcss from '@tailwindcss/vite';
import { tanstackRouter } from '@tanstack/router-plugin/vite';
@@ -7,51 +6,27 @@ import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
// https://vite.dev/config/
import { fileURLToPath } from 'node:url';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
export default defineConfig({
plugins: [tanstackRouter({
target: 'react',
autoCodeSplitting: true
}), react(), tailwindcss(), svgr()],
plugins: [
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
}),
react(),
tailwindcss(),
svgr(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
'@': path.resolve(__dirname, './src'),
},
},
server: {
proxy: {
'/api': 'http://10.0.0.10:8000'
'/api': 'http://10.0.0.10:8000',
},
host: '0.0.0.0',
port: 5173,
allowedHosts: ['nix.org.cn', 'nixos.party']
allowedHosts: ['nix.org.cn', 'nixos.party'],
},
test: {
projects: [{
extends: true,
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({
configDir: path.join(dirname, '.storybook')
})],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [{
browser: 'chromium'
}]
},
setupFiles: ['.storybook/vitest.setup.ts']
}
}]
}
});

View File

@@ -1 +0,0 @@
/// <reference types="@vitest/browser-playwright" />

View File

@@ -20,7 +20,7 @@ cache:
db: 0
service_name: nixcn-cms-redis
search:
host: http://127.0.0.1:7700
host: 127.0.0.1
api_key: ""
service_name: nixcn-cms-meilisearch
email:

View File

@@ -1,13 +0,0 @@
FROM docker.io/golang:1.25.5-alpine AS backend-build
WORKDIR /app
COPY . /app
RUN go install github.com/swaggo/swag/cmd/swag@latest
RUN go mod tidy && \
go generate . && \
go build -o /app/nixcn-cms
FROM docker.io/alpine:3.23
WORKDIR /app
COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms
EXPOSE 8000
ENTRYPOINT [ "/app/nixcn-cms" ]

View File

@@ -1,16 +0,0 @@
FROM docker.io/node:22-alpine AS client-cms-build
RUN apk add just
RUN npm install -g corepack && \
corepack enable
WORKDIR /app
ARG VITE_APP_BASE_URL
ENV VITE_APP_BASE_URL=$VITE_APP_BASE_URL
COPY . .
RUN cd client/cms && pnpm install
RUN cd client/cms && pnpm run build --outDir /app/.outputs/client/cms/dist
FROM docker.io/caddy:2-alpine
COPY ./deploy/client-cms.Caddyfile /etc/caddy/Caddyfile
COPY --from=client-cms-build /app/.outputs/client/cms/dist /srv
EXPOSE 3000
ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

View File

@@ -81,6 +81,9 @@ 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
}
@@ -127,20 +130,12 @@ func (self *User) Create(ctx context.Context) error {
return nil
}
func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID, updates map[string]any) error {
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(updates).Error; err != nil {
if err := tx.Model(&User{}).Where("user_id = ?", userId).Updates(&self).Error; err != nil {
return err
}
var updatedUser User
if err := tx.Where("user_id = ?", userId).First(&updatedUser).Error; err != nil {
return err
}
return updatedUser.UpdateSearchIndex(&ctx)
return nil
})
}

View File

@@ -1,9 +0,0 @@
test.nix.org.cn {
handle /api/* {
reverse_proxy backend:8000
}
handle {
reverse_proxy client-cms:3000
}
}

View File

@@ -1,6 +0,0 @@
:3000 {
root * /srv
encode zstd gzip
try_files {path} /index.html
file_server
}

View File

@@ -1,100 +0,0 @@
services:
postgres:
image: docker.io/postgres:18-alpine
container_name: cms-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- ./data/postgres:/var/lib/postgresql/data
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: cms-redis
volumes:
- ./data/redis:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 3s
retries: 5
meilisearch:
image: getmeili/meilisearch:v1.34.3
container_name: cms-search
environment:
- MEILI_MASTER_KEY=meilisearch
volumes:
- ./data/meilisearch:/meili_data
healthcheck:
test:
["CMD-SHELL", "curl -f http://localhost:7700/health || exit 1"]
interval: 5s
timeout: 3s
retries: 10
lgtm:
image: grafana/otel-lgtm:latest
container_name: lgtm-stack
ports:
- "3000:3000"
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
volumes:
- ./data/lgtm:/data
healthcheck:
test:
[
"CMD-SHELL",
"curl -f http://localhost:3000/api/health || exit 1",
]
interval: 10s
timeout: 5s
retries: 5
caddy:
image: docker.io/caddy:latest
container_name: caddy-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data/caddy/data:/data
- ./data/caddy/config:/config
depends_on:
- backend
- client-cms
client-cms:
image: registry.asnk.io/nixcn-cms/client-cms:dev
container_name: cms-client
restart: always
depends_on:
lgtm:
condition: service_healthy
backend:
image: registry.asnk.io/nixcn-cms/backend:dev
container_name: cms-backend
restart: always
volumes:
- ./config.yaml:/app/config.yaml:ro
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
meilisearch:
condition: service_healthy
lgtm:
condition: service_healthy

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,841 +0,0 @@
basePath: /api/v1
definitions:
data.User:
properties:
allow_public:
type: boolean
avatar:
type: string
bio:
type: string
email:
type: string
id:
type: integer
nickname:
type: string
permission_level:
type: integer
subtitle:
type: string
user_id:
type: string
username:
type: string
uuid:
type: string
type: object
data.UserSearchDoc:
properties:
avatar:
type: string
email:
type: string
nickname:
type: string
subtitle:
type: string
type:
type: string
user_id:
type: string
username:
type: string
type: object
service_auth.ExchangeData:
properties:
client_id:
type: string
redirect_uri:
type: string
state:
type: string
type: object
service_auth.ExchangeResponse:
properties:
redirect_uri:
type: string
type: object
service_auth.MagicData:
properties:
client_id:
type: string
client_ip:
type: string
email:
type: string
redirect_uri:
type: string
state:
type: string
turnstile_token:
type: string
type: object
service_auth.MagicResponse:
properties:
uri:
type: string
type: object
service_auth.RefreshData:
properties:
refresh_token:
type: string
type: object
service_auth.TokenData:
properties:
code:
type: string
type: object
service_auth.TokenResponse:
properties:
access_token:
type: string
refresh_token:
type: string
type: object
service_event.CheckinQueryResponse:
properties:
checkin_at:
type: string
type: object
service_event.CheckinResponse:
properties:
checkin_code:
type: string
type: object
service_event.CheckinSubmitData:
properties:
checkin_code:
type: string
type: object
service_event.InfoResponse:
properties:
end_time:
type: string
name:
type: string
start_time:
type: string
type: object
service_user.UserInfoData:
properties:
allow_public:
type: boolean
avatar:
type: string
bio:
type: string
email:
type: string
nickname:
type: string
permission_level:
type: integer
subtitle:
type: string
user_id:
type: string
username:
type: string
type: object
service_user.UserTableResponse:
properties:
user_table:
items:
$ref: '#/definitions/data.User'
type: array
type: object
utils.RespStatus:
properties:
code:
type: integer
data: {}
error_id:
type: string
status:
type: string
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:
/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
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/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
"500":
description: Internal Server Error
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
security:
- ApiKeyAuth: []
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
security:
- ApiKeyAuth: []
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:
- ApiKeyAuth: []
summary: Submit Check-in Code
tags:
- Event
/event/info:
get:
consumes:
- application/json
description: Fetches the name, start time, and end time of an event using its
UUID.
parameters:
- description: Event UUID
in: query
name: event_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successful retrieval
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
$ref: '#/definitions/service_event.InfoResponse'
type: object
"400":
description: Invalid Input
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:
- ApiKeyAuth: []
summary: Get Event Information
tags:
- Event
/user/full:
get:
consumes:
- application/json
description: Fetches all user records without pagination. This is typically
used for administrative overview or data export.
produces:
- application/json
responses:
"200":
description: Successful retrieval of full user table
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
$ref: '#/definitions/service_user.UserTableResponse'
type: object
"500":
description: Internal Server Error (Database Error)
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
security:
- ApiKeyAuth: []
summary: Get Full User Table
tags:
- User
/user/info:
get:
consumes:
- application/json
description: Fetches the complete profile data for the user associated with
the provided session/token.
produces:
- application/json
responses:
"200":
description: Successful profile retrieval
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
$ref: '#/definitions/service_user.UserInfoData'
type: object
"403":
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:
- ApiKeyAuth: []
summary: Get My User Information
tags:
- User
/user/list:
get:
consumes:
- application/json
description: Fetches a list of users with support for pagination via limit and
offset. Data is sourced from the search engine for high performance.
parameters:
- description: Maximum number of users to return (default 0)
in: query
name: limit
type: string
- description: Number of users to skip
in: query
name: offset
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successful paginated list retrieval
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
items:
$ref: '#/definitions/data.UserSearchDoc'
type: array
type: object
"400":
description: Invalid Input (Format Error)
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
"500":
description: Internal Server Error (Search Engine or Missing Offset)
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
security:
- ApiKeyAuth: []
summary: List Users
tags:
- User
/user/update:
patch:
consumes:
- application/json
description: |-
Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
parameters:
- description: Updated User Profile Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/service_user.UserInfoData'
produces:
- application/json
responses:
"200":
description: Successful profile update
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
"400":
description: Invalid Input (Validation Failed)
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
"403":
description: Missing User ID / Unauthorized
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
"500":
description: Internal Server Error (Database Error / UUID Parse Failed)
schema:
allOf:
- $ref: '#/definitions/utils.RespStatus'
- properties:
data:
type: object
type: object
security:
- ApiKeyAuth: []
summary: Update User Information
tags:
- User
schemes:
- http
- https
swagger: "2.0"

View File

@@ -1,6 +1,3 @@
package main
//go:generate go run ./cmd/gen_exception/main.go
//go:generate swag fmt
//go:generate swag init -g server/server.go

32
go.mod
View File

@@ -17,9 +17,6 @@ require (
github.com/redis/go-redis/extra/redisotel/v9 v9.17.2
github.com/redis/go-redis/v9 v9.17.2
github.com/spf13/viper v1.21.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
@@ -32,7 +29,7 @@ require (
go.opentelemetry.io/otel/sdk/log v0.15.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
golang.org/x/crypto v0.47.0
golang.org/x/crypto v0.46.0
golang.org/x/text v0.33.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
@@ -46,14 +43,13 @@ 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/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.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
@@ -67,21 +63,11 @@ require (
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-playground/validator/v10 v10.29.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
@@ -103,7 +89,7 @@ require (
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.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/sagikazarmark/locafero v0.11.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
@@ -122,11 +108,9 @@ require (
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.41.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
google.golang.org/grpc v1.77.0 // indirect

75
go.sum
View File

@@ -6,8 +6,6 @@ github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeE
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
@@ -63,10 +61,10 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -94,8 +92,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
@@ -109,41 +105,14 @@ 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.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
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-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=
@@ -151,8 +120,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
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/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=
@@ -258,8 +227,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
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=
@@ -304,12 +273,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
@@ -390,8 +353,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -403,8 +366,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -428,8 +389,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
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/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=
@@ -466,8 +427,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -493,6 +454,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -507,8 +470,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"nixcn-cms/internal/cryptography"
"nixcn-cms/internal/kyc"
"unicode/utf8"
alicloudauth20190307 "github.com/alibabacloud-go/cloudauth-20190307/v4/client"
@@ -19,21 +18,21 @@ import (
"github.com/spf13/viper"
)
func DecodeB64Json(b64Json string) (*kyc.KycInfo, error) {
func DecodeB64Json(b64Json string) (*KycInfo, error) {
rawJson, err := base64.StdEncoding.DecodeString(b64Json)
if err != nil {
return nil, errors.New("[KYC] invalid base64 json")
}
var kycInfo kyc.KycInfo
if err := json.Unmarshal(rawJson, &kycInfo); err != nil {
var kyc KycInfo
if err := json.Unmarshal(rawJson, &kyc); err != nil {
return nil, errors.New("[KYC] invalid json structure")
}
return &kycInfo, nil
return &kyc, nil
}
func EncodeAES(kyc *kyc.KycInfo) (*string, error) {
func EncodeAES(kyc *KycInfo) (*string, error) {
plainJson, err := json.Marshal(kyc)
if err != nil {
return nil, err
@@ -48,22 +47,22 @@ func EncodeAES(kyc *kyc.KycInfo) (*string, error) {
return &encrypted, nil
}
func DecodeAES(cipherStr string) (*kyc.KycInfo, error) {
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 kycInfo kyc.KycInfo
if err := json.Unmarshal(plainBytes, &kycInfo); err != nil {
var kyc KycInfo
if err := json.Unmarshal(plainBytes, &kyc); err != nil {
return nil, errors.New("[KYC] invalid decrypted json")
}
return &kycInfo, nil
return &kyc, nil
}
func MD5AliEnc(kyc *kyc.KycInfo) (*KycAli, error) {
func MD5AliEnc(kyc *KycInfo) (*KycAli, error) {
if kyc.Type != "Chinese" {
return nil, nil
}

View File

@@ -11,25 +11,9 @@ 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 /api/v1
// @schemes http https
func Start(ctx context.Context) {
if !viper.GetBool("server.debug_mode") {
gin.SetMode(gin.ReleaseMode)
@@ -39,11 +23,6 @@ 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"))

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

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

15
service/auth/handler.go Normal file
View File

@@ -0,0 +1,15 @@
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)
}

View File

@@ -1,116 +1,91 @@
package service_auth
package 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/pkgs/authcode"
"nixcn-cms/pkgs/email"
"nixcn-cms/pkgs/turnstile"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
type MagicData struct {
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"`
ClientIP string `json:"client_ip"`
}
type MagicPayload struct {
Context context.Context
Data *MagicData
}
func Magic(c *gin.Context) {
// Parse request
var req MagicRequest
if err := c.ShouldBindJSON(&req); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build(c)
utils.HttpResponse(c, 400, errorCode)
return
}
type MagicResponse struct {
Uri string `json:"uri"`
}
type MagicResult struct {
Common shared.CommonResult
Data *MagicResponse
}
func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult) {
var err error
ok, err := turnstile.VerifyTurnstile(payload.Data.TurnstileToken, payload.Data.ClientIP)
// Cloudflare turnstile
ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP())
if err != nil || !ok {
exception := new(exception.Builder).
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicTurnstileFailed).
SetError(err).
Throw(payload.Context)
result = &MagicResult{
Common: shared.CommonResult{
HttpCode: 403,
Exception: exception,
},
Data: nil,
}
Build(c)
utils.HttpResponse(c, 403, errorCode)
return
}
code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, payload.Data.Email)
code, err := authcode.NewAuthCode(c, req.ClientId, req.Email)
if err != nil {
exception := new(exception.Builder).
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicCodeGenFailed).
SetError(err).
Throw(payload.Context)
result = &MagicResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
Build(c)
utils.HttpResponse(c, 500, errorCode)
return
}
externalUrl := viper.GetString("server.external_url")
url, err := url.Parse(externalUrl)
if err != nil {
exception := new(exception.Builder).
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicInvalidExternalUrl).
SetError(err).
Throw(payload.Context)
result = &MagicResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
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", payload.Data.RedirectUri)
query.Set("state", payload.Data.State)
query.Set("client_id", payload.Data.ClientId)
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")
@@ -118,71 +93,37 @@ func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult)
uriData := struct {
Uri string `json:"uri"`
}{url.String()}
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &MagicResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: &MagicResponse{uriData.Uri},
}
utils.HttpResponse(c, 200, "", "magiclink sent", uriData)
return
} else {
// Send email using resend
emailClient, err := new(email.Client).NewSMTPClient()
if err != nil {
exception := new(exception.Builder).
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicInvalidEmailConfig).
SetError(err).
Throw(payload.Context)
result = &MagicResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
Build(c)
utils.HttpResponse(c, 500, errorCode)
return
}
emailClient.Send(
"NixCN CMS <cms@yuri.nix.org.cn>",
payload.Data.Email,
req.Email,
"NixCN CMS Email Verify",
"<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+url.String()+">"+url.String()+"</a>",
)
}
exception := new(exception.Builder).
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &MagicResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: nil,
}
return
Build(c)
utils.HttpResponse(c, 200, errorCode)
}

170
service/auth/redirect.go Normal file
View File

@@ -0,0 +1,170 @@
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())
}

75
service/auth/refresh.go Normal file
View File

@@ -0,0 +1,75 @@
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)
}

Some files were not shown because too many files have changed in this diff Show More