Compare commits

...

43 Commits

Author SHA1 Message Date
2a0788ea86 fix(client): no grayscale on avatar
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-29 09:39:09 +08:00
3ac1f4165f Fix backend Containerfile
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 01:35:56 +08:00
44a97c6d0f Fix backend Containerfile
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 01:32:49 +08:00
c75423bf84 Fix backend Containerfile
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 01:31:23 +08:00
937f382f93 Fix swagger auth docs
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 01:28:19 +08:00
654b196bfd Fix swagger data struct error
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 01:25:12 +08:00
f7bde8ef2e Mod justfile to auto swag init when go files changed
Some checks failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 01:00:51 +08:00
732d9866db Generate swagger docs
Some checks failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 00:59:49 +08:00
330b037dca Fix stupid ai bug
Some checks failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 00:58:28 +08:00
79dfa8499c Full Restruct API and Services
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-29 00:45:58 +08:00
89e7f1a41a WIP: Restructing auth api and service
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 18:19:24 +08:00
e3c0b60337 Fix client-cms Containerfile
Some checks failed
Backend Check Build (NixCN CMS) TeamCity build failed
Client CMS Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 16:47:27 +08:00
140a3070d6 Fix client-cms Containerfile
Some checks failed
Backend Build (NixCN CMS) TeamCity build failed
Client CMS Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 10:42:47 +08:00
a56333fda8 Fix client-cms Containerfile
Some checks failed
Backend Build (NixCN CMS) TeamCity build failed
Client CMS Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 10:07:40 +08:00
2b5f55f359 Fix client-cms Containerfile
Some checks failed
Client CMS Build (NixCN CMS) TeamCity build failed
Backend Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 10:05:21 +08:00
e480bd6548 Fix client-cms Containerfile
Some checks failed
Client CMS Build (NixCN CMS) TeamCity build failed
Backend Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 10:01:41 +08:00
9fb67ce2be Fix client-cms Containerfile
Some checks failed
Client CMS Build (NixCN CMS) TeamCity build failed
Backend Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 09:59:56 +08:00
1c7192db17 Fix client-cms Containerfile
Some checks failed
Backend Build (NixCN CMS) TeamCity build failed
Client CMS Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 09:54:54 +08:00
9ded703143 Fix client-cms Containerfile
Some checks failed
Client CMS Build (NixCN CMS) TeamCity build failed
Backend Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 09:45:46 +08:00
3f535a8249 Split containerfile, move to container folder
Some checks failed
Client CMS Build (NixCN CMS) TeamCity build failed
Backend Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 09:23:59 +08:00
18fa741e4d Remove gitea actions
Some checks failed
Backend Build (NixCN CMS) TeamCity build failed
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 02:05:15 +08:00
4cd4a8cae6 Fix gitea workflows
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Waiting to run
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:53:36 +08:00
d90e22b641 Fix gitea workflow name
Some checks failed
Check build frontend and backend / Build PNPM Frontend (push) Failing after 1m15s
Check build frontend and backend / Build Go Backend (push) Has been cancelled
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:48:18 +08:00
4f7632af53 Fix gitea workflow
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:41:39 +08:00
ca080f4e2a Add gitea action workflow
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-28 01:36:54 +08:00
5a5239e335 Optomize user list service query bind struct
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-24 12:48:11 +08:00
314995e5f9 Finilize user api layer
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-24 12:37:17 +08:00
8e11ba4631 WIP: Full restruct, seprate service and api
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-24 11:42:35 +08:00
dfd5532b20 Change default config
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:39:20 +08:00
986f63c0af Add context for all exceptions
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:37:20 +08:00
154c929859 Change postgres db instance name
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:25:58 +08:00
f779435cf0 Devenv backend wait for 30s to boot
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:22:07 +08:00
5f6eb9f2a2 Trace back everything (tested)
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 19:19:17 +08:00
3f44d2d9c2 Add otel tracer
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 16:59:53 +08:00
b8f89ab655 Add context for everything
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 16:43:46 +08:00
83df018d34 Only enable file log in debug mode
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:52:44 +08:00
7b3fe24b7c Add ErrorHandler for log level selects
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:48:49 +08:00
75c4edfa3d fix(client): remove console.log
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 15:38:31 +08:00
a060901cc3 refactor(client): improve token handler stability
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 15:37:04 +08:00
8e41514d05 Fix stupid ai bug
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:30:50 +08:00
9aff7d8f26 Fix 200 response exception builder
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 15:26:59 +08:00
2f26b2ddb5 Fix stupid ai errors
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-21 14:58:23 +08:00
96d76b3657 feat(client): bio editor
Signed-off-by: Noa Virellia <noa@requiem.garden>
2026-01-21 14:51:03 +08:00
101 changed files with 7546 additions and 1529 deletions

View File

@@ -1,26 +0,0 @@
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" ]

86
api/auth/exchange.go Normal file
View File

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

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

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

54
api/auth/magic.go Normal file
View File

@@ -0,0 +1,54 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Magic handles the "Magic Link" authentication request.
// @Summary Request Magic Link
// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.MagicData true "Magic Link Request Data"
// @Success 200 {object} 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)
}

60
api/auth/redirect.go Normal file
View File

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

52
api/auth/refresh.go Normal file
View File

@@ -0,0 +1,52 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Refresh handles the token rotation process.
// @Summary Refresh Access Token
// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token.
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.RefreshData true "Refresh Token Body"
// @Success 200 {object} 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)
}

52
api/auth/token.go Normal file
View File

@@ -0,0 +1,52 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_auth"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
// Token exchanges an authorization code for access and refresh tokens.
// @Summary Exchange Code for Token
// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
// @Tags Authentication
// @Accept json
// @Produce json
// @Param payload body service_auth.TokenData true "Token Request Body"
// @Success 200 {object} 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)
}

121
api/event/checkin.go Normal file
View File

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

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

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

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

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

17
api/handler.go Normal file
View File

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

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

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

View File

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

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

@@ -0,0 +1,34 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"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{
Context: c,
}
result := self.svc.GetUserFullTable(userTablePayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

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

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

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

@@ -0,0 +1,68 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"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 {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Throw(c).
String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 500, errorCode)
return
}
UserInfoPayload := &service_user.UserInfoPayload{
Context: c,
UserId: userId,
Data: nil,
}
result := self.svc.GetUserInfo(UserInfoPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

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

@@ -0,0 +1,59 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"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"`
Offset *string `form:"offset"`
}
var query ListQuery
if err := c.ShouldBindQuery(&query); err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Throw(c).
String()
utils.HttpResponse(c, 400, exception)
return
}
userListPayload := &service_user.UserListPayload{
Context: c,
Limit: query.Limit,
Offset: query.Offset,
}
result := self.svc.ListUsers(userListPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

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

@@ -0,0 +1,83 @@
package user
import (
"nixcn-cms/internal/exception"
"nixcn-cms/service/service_user"
"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 {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Throw(c).
String()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
Throw(c).
String()
utils.HttpResponse(c, 500, errorCode)
return
}
userInfoPayload := &service_user.UserInfoPayload{
Context: c,
UserId: userId,
}
err = c.ShouldBindJSON(&userInfoPayload.Data)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(c).
String()
utils.HttpResponse(c, 400, errorCode)
return
}
result := self.svc.UpdateUserInfo(userInfoPayload)
if result.Common.Exception.Original != exception.CommonSuccess {
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
return
}
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
}

View File

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

View File

@@ -37,6 +37,7 @@
"@tanstack/react-table": "^8.21.3",
"@tanstack/zod-adapter": "^1.143.4",
"@tanstack/zod-form-adapter": "^0.42.1",
"@uiw/react-md-editor": "^4.0.11",
"axios": "^1.13.2",
"base-64": "^1.0.0",
"buffer": "^6.0.3",

View File

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

View File

@@ -1,12 +1,22 @@
import { Mail } from 'lucide-react';
import MDEditor from '@uiw/react-md-editor';
import { isNil } from 'lodash-es';
import { Mail, Pencil } from 'lucide-react';
import { useState } from 'react';
import Markdown from 'react-markdown';
import { toast } from 'sonner';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { base64ToUtf8 } from '@/lib/utils';
import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
import { Button } from '../ui/button';
import { EditProfileDialog } from './edit-profile-dialog';
export function MainProfile() {
const { data: user } = useUserInfo();
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio));
const [enableBioEdit, setEnableBioEdit] = useState(false);
const { mutateAsync } = useUpdateUser();
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">
@@ -30,9 +40,42 @@ export function MainProfile() {
<EditProfileDialog />
</div>
</div>
<section className="rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 p-6 prose dark:prose-invert max-w-[1012px] self-center">
<section className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
{/* Bio */}
<Markdown>{base64ToUtf8(user.bio)}</Markdown>
{enableBioEdit
? (
<MDEditor
value={bio}
onChange={setBio}
height="100%"
/>
)
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
<Button
className="absolute bottom-4 right-4"
// eslint-disable-next-line ts/no-misused-promises
onClick={async () => {
if (!enableBioEdit) {
setEnableBioEdit(true);
}
else {
if (!isNil(bio)) {
try {
await mutateAsync({ bio: utf8ToBase64(bio) });
setEnableBioEdit(false);
}
catch (error) {
console.error(error);
toast.error('个人简介更新失败');
}
}
}
}}
size="icon-sm"
variant={enableBioEdit ? 'default' : 'outline'}
>
<Pencil />
</Button>
</section>
</div>
);

View File

@@ -41,7 +41,7 @@ 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 grayscale">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.nickname} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>

View File

@@ -30,9 +30,15 @@ export function clearTokens() {
}
export async function doSetTokenByCode(code: string) {
const { data } = await axiosClient.post<{ access_token: string; refresh_token: string }>('/auth/token', { code }, { headers: HEADER_API_VERSION });
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);
});
});
}
export async function doRefreshToken() {

View File

@@ -1,5 +1,5 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import z from 'zod';
import { doSetTokenByCode } from '@/lib/token';
@@ -16,10 +16,15 @@ function RouteComponent() {
const { code } = Route.useSearch();
const [status, setStatus] = useState('Loading...');
const navigate = useNavigate();
useEffect(() => {
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,4 +1,5 @@
common:
success: "00000"
error:
invalid_input: "00001"
unauthorized: "00002"

View File

@@ -3,21 +3,26 @@ server:
address: :8000
external_url: https://example.com
debug_mode: false
log_level: debug
service_name: nixcn-cms-backend
database:
type: postgres
host: 127.0.0.1
name: postgres
username: postgres
password: postgres
service_name: nixcn-cms-postgres
cache:
hosts: ["127.0.0.1:6379"]
master: ""
username: ""
password: ""
db: 0
service_name: nixcn-cms-redis
search:
host: 127.0.0.1
api_key: ""
service_name: nixcn-cms-meilisearch
email:
host:
port:
@@ -37,3 +42,5 @@ ttl:
kyc:
ali_access_key_id: example
ali_access_key_secret: example
tracer:
otel_controller_endpoint: localhost:4317

View File

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

View File

@@ -9,6 +9,7 @@ type config struct {
Secrets secrets `yaml:"secrets"`
TTL ttl `yaml:"ttl"`
KYC kyc `yaml:"kyc"`
Tracer tracer `yaml:"tracer"`
}
type server struct {
@@ -16,6 +17,8 @@ type server struct {
Address string `yaml:"address"`
ExternalUrl string `yaml:"external_url"`
DebugMode string `yaml:"debug_mode"`
LogLevel string `yaml:"log_level"`
ServiceName string `yaml:"service_name"`
}
type database struct {
@@ -24,6 +27,7 @@ type database struct {
Name string `yaml:"name"`
Username string `yaml:"username"`
Password string `yaml:"password"`
ServiceName string `yaml:"service_name"`
}
type cache struct {
@@ -32,11 +36,13 @@ type cache struct {
Username string `yaml:"username"`
Password string `yaml:"passowrd"`
DB int `yaml:"db"`
ServiceName string `yaml:"service_name"`
}
type search struct {
Host string `yaml:"host"`
ApiKey string `yaml:"api_key"`
ServiceName string `yaml:"service_name"`
}
type email struct {
@@ -65,3 +71,7 @@ type kyc struct {
AliAccessKeyId string `yaml:"ali_access_key_id"`
AliAccessKeySecret string `yaml:"ali_access_key_secret"`
}
type tracer struct {
OtelControllerEndpoint string `yaml:"otel_controller_endpoint"`
}

View File

@@ -0,0 +1,13 @@
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

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

View File

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

View File

@@ -1,6 +1,7 @@
package data
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
@@ -22,9 +23,9 @@ type Client struct {
RedirectUri datatypes.JSON `json:"redirect_uri" gorm:"type:json;not null"`
}
func (self *Client) GetClientByClientId(clientId string) (*Client, error) {
func (self *Client) GetClientByClientId(ctx context.Context, clientId string) (*Client, error) {
var client Client
if err := Database.
if err := Database.WithContext(ctx).
Where("client_id = ?", clientId).
First(&client).Error; err != nil {
return nil, err
@@ -44,7 +45,7 @@ type ClientParams struct {
RedirectUri []string
}
func (self *Client) Create(params *ClientParams) (*Client, error) {
func (self *Client) Create(ctx context.Context, params *ClientParams) (*Client, error) {
jsonRedirectUri, err := json.Marshal(params.RedirectUri)
if err != nil {
return nil, err
@@ -69,7 +70,7 @@ func (self *Client) Create(params *ClientParams) (*Client, error) {
RedirectUri: jsonRedirectUri,
}
if err := Database.Create(&client).Error; err != nil {
if err := Database.WithContext(ctx).Create(&client).Error; err != nil {
return nil, err
}

View File

@@ -1,6 +1,7 @@
package data
import (
"context"
"nixcn-cms/data/drivers"
"os"
@@ -16,7 +17,7 @@ var Database *gorm.DB
var Redis redis.UniversalClient
var MeiliSearch meilisearch.ServiceManager
func Init() {
func Init(ctx context.Context) {
// Init database
dbType := viper.GetString("database.type")
exDSN := drivers.ExternalDSN{
@@ -27,21 +28,21 @@ func Init() {
}
if dbType != "postgres" {
slog.Error("[Database] Only support postgras db!")
slog.ErrorContext(ctx, "[Database] Only support postgras db!")
os.Exit(1)
}
// Conect to db
db, err := drivers.Postgres(exDSN)
if err != nil {
slog.Error("[Database] Error connecting to db!", "err", err)
slog.ErrorContext(ctx, "[Database] Error connecting to db!", "err", err)
os.Exit(1)
}
// Auto migrate
err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{})
if err != nil {
slog.Error("[Database] Error migrating database!", "err", err)
slog.ErrorContext(ctx, "[Database] Error migrating database!", "err", err)
os.Exit(1)
}
Database = db
@@ -57,7 +58,7 @@ func Init() {
}
rdb, err := drivers.Redis(rDSN)
if err != nil {
slog.Error("[Redis] Error connecting to Redis!", "err", err)
slog.ErrorContext(ctx, "[Redis] Error connecting to Redis!", "err", err)
os.Exit(1)
}
Redis = rdb

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1479
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

1450
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

826
docs/swagger.yaml Normal file
View File

@@ -0,0 +1,826 @@
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
info:
contact: {}
paths:
/auth/exchange:
post:
consumes:
- application/json
description: Exchanges client credentials and user session for a specific redirect
authorization code.
parameters:
- description: Exchange Request Credentials
in: body
name: payload
required: true
schema:
$ref: '#/definitions/service_auth.ExchangeData'
produces:
- application/json
responses:
"200":
description: 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
swagger: "2.0"

View File

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

74
go.mod
View File

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

207
go.sum
View File

@@ -2,6 +2,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo=
github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo=
github.com/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=
@@ -57,10 +63,12 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -78,24 +86,64 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-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.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -103,8 +151,9 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -124,10 +173,16 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -136,6 +191,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -152,8 +211,14 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -177,22 +242,38 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 h1:KYWnHK9pwzOUo3sNJlNmzRwZ5mw7opugn8njtGThKNg=
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2/go.mod h1:wsfMQVl/GFYD9Gx/tlxurlTtvHkZRAt8j1qi27eIlTk=
github.com/redis/go-redis/extra/redisotel/v9 v9.17.2 h1:wthFPRW3Y50CknMrjjJoYwXUFR4U7hMVJCMeLzDI8s4=
github.com/redis/go-redis/extra/redisotel/v9 v9.17.2/go.mod h1:iqfQX7U2o8MWSl8W+Ah8KqbQyi/UoR/MQNgvaUyA1wc=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -213,6 +294,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -222,6 +304,13 @@ 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=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@@ -229,11 +318,57 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0 h1:7IKZbAYwlwLXAdu7SVPhzTjDjogWZxP4MIa7rovY+PU=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0/go.mod h1:+TF5nf3NIv2X8PGxqfYOaRnAoMM43rUA2C3XsN2DoWA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g=
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -247,6 +382,7 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
@@ -254,18 +390,21 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.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=
@@ -275,7 +414,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -287,14 +428,16 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
@@ -309,6 +452,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -322,8 +466,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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -339,6 +483,7 @@ golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@@ -348,8 +493,6 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.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=
@@ -358,32 +501,48 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -399,8 +558,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/clickhouse v0.7.0 h1:BCrqvgONayvZRgtuA6hdya+eAW5P2QVagV3OlEp1vtA=
gorm.io/driver/clickhouse v0.7.0/go.mod h1:TmNo0wcVTsD4BBObiRnCahUgHJHjBIwuRejHwYt3JRs=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
@@ -410,5 +571,7 @@ gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOze
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc=
gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

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

View File

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

View File

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

View File

@@ -39,11 +39,11 @@ func (self *Token) NewClaims(clientId string, userId uuid.UUID) JwtClaims {
}
// Generate access token
func (self *Token) GenerateAccessToken(clientId string, userId uuid.UUID) (string, error) {
func (self *Token) GenerateAccessToken(ctx context.Context, clientId string, userId uuid.UUID) (string, error) {
claims := self.NewClaims(clientId, userId)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
clientData, err := new(data.Client).GetClientByClientId(clientId)
clientData, err := new(data.Client).GetClientByClientId(ctx, clientId)
if err != nil {
return "", fmt.Errorf("error getting client data: %v", err)
}
@@ -70,9 +70,9 @@ func (self *Token) GenerateRefreshToken() (string, error) {
}
// Issue both access and refresh token
func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, string, error) {
func (self *Token) IssueTokens(ctx context.Context, clientId string, userId uuid.UUID) (string, string, error) {
// access token
access, err := self.GenerateAccessToken(clientId, userId)
access, err := self.GenerateAccessToken(ctx, clientId, userId)
if err != nil {
return "", "", err
}
@@ -83,7 +83,6 @@ func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, strin
return "", "", err
}
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl")
refreshKey := "refresh:" + refresh
@@ -122,8 +121,7 @@ func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, strin
}
// Refresh access token
func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
ctx := context.Background()
func (self *Token) RefreshAccessToken(ctx context.Context, refreshToken string) (string, error) {
key := "refresh:" + refreshToken
// read refresh token bind data
@@ -145,11 +143,10 @@ func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
}
// Generate new access token
return self.GenerateAccessToken(clientId, userId)
return self.GenerateAccessToken(ctx, clientId, userId)
}
func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
ctx := context.Background()
func (self *Token) RenewRefreshToken(ctx context.Context, refreshToken string) (string, error) {
ttl := viper.GetDuration("ttl.refresh_ttl")
oldKey := "refresh:" + refreshToken
@@ -174,7 +171,7 @@ func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
}
// revoke old refresh token
if err := self.RevokeRefreshToken(refreshToken); err != nil {
if err := self.RevokeRefreshToken(ctx, refreshToken); err != nil {
return "", err
}
@@ -211,9 +208,7 @@ func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
return newRefresh, nil
}
func (self *Token) RevokeRefreshToken(refreshToken string) error {
ctx := context.Background()
func (self *Token) RevokeRefreshToken(ctx context.Context, refreshToken string) error {
refreshKey := "refresh:" + refreshToken
// read refresh token metadata (user_id, client_id)
@@ -246,7 +241,7 @@ func (self *Token) RevokeRefreshToken(refreshToken string) error {
return err
}
func (self *Token) HeaderVerify(header string) (string, error) {
func (self *Token) HeaderVerify(ctx context.Context, header string) (string, error) {
if header == "" {
return "", nil
}
@@ -273,7 +268,7 @@ func (self *Token) HeaderVerify(header string) (string, error) {
return nil, errors.New("[Auth Token] client_id missing in token")
}
clientData, err := new(data.Client).GetClientByClientId(claims.ClientId)
clientData, err := new(data.Client).GetClientByClientId(ctx, claims.ClientId)
if err != nil {
return nil, fmt.Errorf("error getting client data: %v", err)
}

View File

@@ -1,8 +1,8 @@
package exception
import (
"context"
"fmt"
"log/slog"
)
// 12 chars len
@@ -19,6 +19,7 @@ type Builder struct {
Type string
Original string
Error error
ErrorCode string
}
func (self *Builder) SetStatus(s string) *Builder {
@@ -51,16 +52,25 @@ func (self *Builder) SetError(e error) *Builder {
return self
}
func (self *Builder) Build() string {
errorCode := fmt.Sprintf("%s%s%s%s%s",
func (self *Builder) build() {
self.ErrorCode = fmt.Sprintf("%s%s%s%s%s",
self.Status,
self.Service,
self.Endpoint,
self.Type,
self.Original,
)
if self.Error != nil {
slog.Warn("Service exception", "id", errorCode, "err", self.Error)
}
return errorCode
}
func (self *Builder) Throw(ctx context.Context) *Builder {
self.build()
if self.Error != nil {
ErrorHandler(ctx, self.Status, self.ErrorCode, self.Error)
}
return self
}
func (self *Builder) String() string {
self.build()
return self.ErrorCode
}

View File

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

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

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

View File

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

View File

@@ -48,4 +48,4 @@ dev-client-cms: install-cms
devenv up client-cms --verbose
dev-back: clean install-back gen-back
devenv up backend postgres redis meilisearch --verbose
devenv up backend postgres redis meilisearch lgtm --verbose

28
logger/gorm.go Normal file
View File

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

View File

@@ -1,14 +1,63 @@
package logger
import (
"context"
"io"
"log/slog"
"os"
"strings"
"github.com/spf13/viper"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel/trace"
)
type multiHandler struct {
handlers []slog.Handler
}
func (m *multiHandler) Enabled(ctx context.Context, l slog.Level) bool {
for _, h := range m.handlers {
if h.Enabled(ctx, l) {
return true
}
}
return false
}
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
span := trace.SpanFromContext(ctx)
if span.SpanContext().HasTraceID() {
r.AddAttrs(
slog.String("trace_id", span.SpanContext().TraceID().String()),
slog.String("span_id", span.SpanContext().SpanID().String()),
)
}
for _, h := range m.handlers {
_ = h.Handle(ctx, r)
}
return nil
}
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newHandlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
newHandlers[i] = h.WithAttrs(attrs)
}
return &multiHandler{handlers: newHandlers}
}
func (m *multiHandler) WithGroup(name string) slog.Handler {
newHandlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
newHandlers[i] = h.WithGroup(name)
}
return &multiHandler{handlers: newHandlers}
}
func Init() {
levelStr := strings.ToLower(os.Getenv("LOG_LEVEL"))
levelStr := strings.ToLower(viper.GetString("server.log_level"))
var level slog.Level
switch levelStr {
case "debug":
@@ -21,15 +70,13 @@ func Init() {
level = slog.LevelInfo
}
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
var writer io.Writer = os.Stdout
if err != nil {
slog.Error("Error to create log file", "err", err)
} else {
if level == slog.LevelDebug {
file, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
writer = io.MultiWriter(os.Stdout, file)
}
opts := &slog.HandlerOptions{
localHandler := slog.NewJSONHandler(writer, &slog.HandlerOptions{
Level: level,
AddSource: true,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
@@ -38,10 +85,13 @@ func Init() {
}
return a
},
})
otelHandler := otelslog.NewHandler(viper.GetString("server.service_name"))
combinedHandler := &multiHandler{
handlers: []slog.Handler{localHandler, otelHandler},
}
handler := slog.NewJSONHandler(writer, opts)
logger := slog.New(handler)
slog.SetDefault(logger)
slog.SetDefault(slog.New(combinedHandler))
}

22
main.go
View File

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

View File

@@ -17,7 +17,8 @@ func ApiVersionCheck() gin.HandlerFunc {
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeSpecific).
SetOriginal(exception.ApiVersionNotFound).
Build()
Throw(c).
String()
utils.HttpAbort(c, 400, errorCode)
return
}

View File

@@ -3,6 +3,7 @@ package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log/slog"
"time"
@@ -24,6 +25,8 @@ func GinLogger() gin.HandlerFunc {
c.Next()
ctx := c.Request.Context()
var errorMessage string
if len(c.Errors) > 0 {
errorMessage = c.Errors.String()
@@ -43,11 +46,11 @@ func GinLogger() gin.HandlerFunc {
status := c.Writer.Status()
if len(c.Errors) > 0 || status >= 500 {
slog.Error("HTTP_ERROR", fields...)
slog.ErrorContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
} else if status >= 400 {
slog.Warn("HTTP_CLIENT_ERROR", fields...)
slog.WarnContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
} else {
slog.Info("HTTP_SUCCESS", fields...)
slog.InfoContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
}
}
}

View File

@@ -1,8 +1,8 @@
package middleware
import (
"nixcn-cms/internal/authtoken"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
@@ -14,16 +14,17 @@ func JWTAuth() gin.HandlerFunc {
auth := c.GetHeader("Authorization")
authtoken := new(authtoken.Token)
uid, err := authtoken.HeaderVerify(auth)
uid, err := authtoken.HeaderVerify(c, auth)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetStatus(exception.StatusUser).
SetService(exception.MiddlewareServiceJwt).
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUnauthorized).
SetError(err).
Build()
Throw(c).
String()
utils.HttpAbort(c, 401, errorCode)
return

View File

@@ -22,7 +22,9 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build()
Throw(c).
String()
utils.HttpAbort(c, 401, errorCode)
return
}
@@ -36,12 +38,13 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
Throw(c).
String()
utils.HttpAbort(c, 500, errorCode)
return
}
userData, err := new(data.User).GetByUserId(userId)
userData, err := new(data.User).GetByUserId(c, &userId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
@@ -50,7 +53,9 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Build()
Throw(c).
String()
utils.HttpAbort(c, 404, errorCode)
return
}
@@ -68,7 +73,9 @@ func Permission(requiredLevel uint) gin.HandlerFunc {
SetEndpoint(exception.EndpointMiddlewareService).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorPermissionDenied).
Build()
Throw(c).
String()
utils.HttpAbort(c, 403, errorCode)
return
}

View File

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

View File

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

View File

@@ -1,37 +1,62 @@
package server
import (
"context"
"log/slog"
"net"
"net/http"
"nixcn-cms/api"
"nixcn-cms/middleware"
"time"
"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"
)
func Start() {
// @title NixCN CMS API
// @version 1.0
// @description API Docs based on Gin framework
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
// @schemes http https
func Start(ctx context.Context) {
if !viper.GetBool("server.debug_mode") {
gin.SetMode(gin.ReleaseMode)
gin.DisableConsoleColor()
}
r := gin.New()
r.Use(gin.Recovery(), middleware.GinLogger())
r.Use(otelgin.Middleware(viper.GetString("server.service_name")))
r.Use(middleware.GinLogger())
r.Use(gin.Recovery())
Router(r)
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
api.Handler(r.Group("/api/v1"))
// Start http server
server := &http.Server{
Addr: viper.GetString("server.address"),
Handler: r,
BaseContext: func(net.Listener) context.Context { return ctx },
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
slog.Info("[Server] Starting server on " + viper.GetString("server.address"))
slog.InfoContext(ctx, "[Server] Starting server on "+viper.GetString("server.address"))
if err := server.ListenAndServe(); err != nil {
slog.Error("[Server] Error starting server!", "err", err)
slog.ErrorContext(ctx, "[Server] Error starting server!", "err", err)
}
}

View File

@@ -1,117 +0,0 @@
package auth
import (
"fmt"
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const ()
func Exchange(c *gin.Context) {
var exchangeReq struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
}
err := c.ShouldBindJSON(&exchangeReq)
if err != nil {
fmt.Println(err)
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUnauthorized).
Build()
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()
utils.HttpResponse(c, 500, errorCode)
return
}
userData := new(data.User)
user, err := userData.GetByUserId(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()
utils.HttpResponse(c, 500, errorCode)
return
}
code, err := authcode.NewAuthCode(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()
utils.HttpResponse(c, 500, errorCode)
return
}
url, err := url.Parse(exchangeReq.RedirectUri)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeInvalidRedirectUri).
SetError(err).
Build()
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()}
utils.HttpResponse(c, 200, "", "success", exchangeResp)
}

View File

@@ -1,15 +0,0 @@
package auth
import (
"nixcn-cms/middleware"
"github.com/gin-gonic/gin"
)
func Handler(r *gin.RouterGroup) {
r.GET("/redirect", Redirect)
r.POST("/magic", middleware.ApiVersionCheck(), Magic)
r.POST("/token", middleware.ApiVersionCheck(), Token)
r.POST("/refresh", middleware.ApiVersionCheck(), Refresh)
r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), Exchange)
}

View File

@@ -1,122 +0,0 @@
package auth
import (
"net/url"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/email"
"nixcn-cms/pkgs/turnstile"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
type MagicRequest struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Email string `json:"email"`
TurnstileToken string `json:"turnstile_token"`
}
func Magic(c *gin.Context) {
// Parse request
var req MagicRequest
if err := c.ShouldBindJSON(&req); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
// Cloudflare turnstile
ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP())
if err != nil || !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicTurnstileFailed).
SetError(err).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
code, err := authcode.NewAuthCode(req.ClientId, req.Email)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicCodeGenFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
externalUrl := viper.GetString("server.external_url")
url, err := url.Parse(externalUrl)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicInvalidExternalUrl).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
url.Path = "/api/v1/auth/redirect"
query := url.Query()
query.Set("code", code)
query.Set("redirect_uri", req.RedirectUri)
query.Set("state", req.State)
query.Set("client_id", req.ClientId)
url.RawQuery = query.Encode()
debugMode := viper.GetBool("server.debug_mode")
if debugMode {
uriData := struct {
Uri string `json:"uri"`
}{url.String()}
utils.HttpResponse(c, 200, "", "magiclink sent", uriData)
return
} else {
// Send email using resend
emailClient, err := new(email.Client).NewSMTPClient()
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceMagic).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthMagicInvalidEmailConfig).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
emailClient.Send(
"NixCN CMS <cms@yuri.nix.org.cn>",
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>",
)
}
utils.HttpResponse(c, 200, "", "magic link sent")
}

View File

@@ -1,170 +0,0 @@
package auth
import (
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
)
func Redirect(c *gin.Context) {
clientId := c.Query("client_id")
if clientId == "" {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
redirectUri := c.Query("redirect_uri")
if redirectUri == "" {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
state := c.Query("state")
if state == "" {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
code := c.Query("code")
// Verify email token
authCode, ok := authcode.VerifyAuthCode(code)
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectTokenInvalid).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
// Verify if user exists
userData := new(data.User)
user, err := userData.GetByEmail(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(); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Build()
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()
utils.HttpResponse(c, 500, errorCode)
return
}
}
clientData := new(data.Client)
client, err := clientData.GetClientByClientId(clientId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectClientNotFound).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
err = client.ValidateRedirectURI(redirectUri)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectUriMismatch).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
newCode, err := authcode.NewAuthCode(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()
utils.HttpResponse(c, 500, errorCode)
return
}
url, err := url.Parse(redirectUri)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectInvalidUri).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
query := url.Query()
query.Set("code", newCode)
url.RawQuery = query.Encode()
c.Redirect(302, url.String())
}

View File

@@ -1,68 +0,0 @@
package auth
import (
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
func Refresh(c *gin.Context) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.ShouldBindJSON(&req); err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
JwtTool := authtoken.Token{
Application: viper.GetString("server.application"),
}
accessToken, err := JwtTool.RefreshAccessToken(req.RefreshToken)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRefreshInvalidToken).
SetError(err).
Build()
utils.HttpResponse(c, 401, errorCode)
return
}
refreshToken, err := JwtTool.RenewRefreshToken(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()
utils.HttpResponse(c, 500, errorCode)
return
}
tokenResp := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}{accessToken, refreshToken}
utils.HttpResponse(c, 200, "", "success", tokenResp)
}

View File

@@ -1,87 +0,0 @@
package auth
import (
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
type TokenRequest struct {
Code string `json:"code"`
}
func Token(c *gin.Context) {
var req TokenRequest
err := c.ShouldBindJSON(&req)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
authCode, ok := authcode.VerifyAuthCode(req.Code)
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthTokenInvalidToken).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
userData := new(data.User)
user, err := userData.GetByEmail(authCode.Email)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
// Generate jwt
JwtTool := authtoken.Token{
Application: viper.GetString("server.application"),
}
accessToken, refreshToken, err := JwtTool.IssueTokens(authCode.ClientId, user.UserId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthTokenGenFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
tokenResp := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}{accessToken, refreshToken}
utils.HttpResponse(c, 200, "", "success", tokenResp)
}

View File

@@ -1,200 +0,0 @@
package event
import (
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Checkin(c *gin.Context) {
data := new(data.Attendance)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
}
// Get event id from query
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
// Parse event id to uuid
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
data.UserId = userId
code, err := data.GenCheckinCode(eventId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventCheckinGenCodeFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
checkinCodeResp := struct {
CheckinCode *string `json:"checkin_code"`
}{code}
utils.HttpResponse(c, 200, "", "success", checkinCodeResp)
}
func CheckinSubmit(c *gin.Context) {
var req struct {
ChekinCode string `json:"checkin_code"`
}
c.ShouldBindJSON(&req)
attendanceData := new(data.Attendance)
err := attendanceData.VerifyCheckinCode(req.ChekinCode)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
utils.HttpResponse(c, 200, "", "success")
}
func CheckinQuery(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
attendanceData := new(data.Attendance)
attendance, err := attendanceData.GetAttendance(userId, eventId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
} else if attendance == nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventCheckinQueryRecordNotFound).
Build()
utils.HttpResponse(c, 404, errorCode)
return
} else if attendance.CheckinAt.IsZero() {
utils.HttpResponse(c, 200, "", "success", gin.H{"checkin_at": nil})
return
}
checkInAtResp := struct {
CheckinAt time.Time `json:"checkin_at"`
}{attendance.CheckinAt}
utils.HttpResponse(c, 200, "", "success", checkInAtResp)
}

View File

@@ -1 +0,0 @@
package event

View File

@@ -1,15 +0,0 @@
package event
import (
"nixcn-cms/middleware"
"github.com/gin-gonic/gin"
)
func Handler(r *gin.RouterGroup) {
r.Use(middleware.JWTAuth(), middleware.Permission(10))
r.GET("/info", Info)
r.GET("/checkin", Checkin)
r.GET("/checkin/query", CheckinQuery)
r.POST("/checkin/submit", middleware.Permission(20), CheckinSubmit)
}

View File

@@ -1,64 +0,0 @@
package event
import (
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Info(c *gin.Context) {
eventData := new(data.Event)
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
// Parse event id
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
event, err := eventData.GetEventById(eventId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventInfoNotFound).
SetError(err).
Build()
utils.HttpResponse(c, 404, errorCode)
return
}
eventInfoResp := struct {
Name string `json:"name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}{event.Name, event.StartTime, event.EndTime}
utils.HttpResponse(c, 200, "", "success", eventInfoResp)
}

View File

@@ -1 +0,0 @@
package event

View File

@@ -1 +0,0 @@
package event

View File

@@ -0,0 +1,129 @@
package service_auth
import (
"context"
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/authcode"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"github.com/google/uuid"
)
type ExchangeData struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
}
type ExchangePayload struct {
Context context.Context
UserId uuid.UUID
Data *ExchangeData
}
type ExchangeResponse struct {
RedirectUri string `json:"redirect_uri"`
}
type ExchangeResult struct {
Common shared.CommonResult
Data *ExchangeResponse
}
func (self *AuthServiceImpl) Exchange(payload *ExchangePayload) (result *ExchangeResult) {
var err error
userData, err := new(data.User).
GetByUserId(payload.Context, &payload.UserId)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeGetUserIdFailed).
SetError(err).
Throw(payload.Context)
result = &ExchangeResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, userData.Email)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeCodeGenFailed).
SetError(err).
Throw(payload.Context)
result = &ExchangeResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
url, err := url.Parse(payload.Data.RedirectUri)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthExchangeInvalidRedirectUri).
SetError(err).
Throw(payload.Context)
result = &ExchangeResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exception,
},
Data: nil,
}
return
}
query := url.Query()
query.Set("code", code)
url.RawQuery = query.Encode()
exception := new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceExchange).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
resultData := &ExchangeResponse{url.String()}
result = &ExchangeResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: resultData,
}
return
}

View File

@@ -0,0 +1,188 @@
package service_auth
import (
"context"
"net/url"
"nixcn-cms/internal/authcode"
"nixcn-cms/internal/email"
"nixcn-cms/internal/exception"
"nixcn-cms/internal/turnstile"
"nixcn-cms/service/shared"
"github.com/spf13/viper"
)
type MagicData struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Email string `json:"email"`
TurnstileToken string `json:"turnstile_token"`
ClientIP string `json:"client_ip"`
}
type MagicPayload struct {
Context context.Context
Data *MagicData
}
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)
if err != nil || !ok {
exception := 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,
}
return
}
code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, payload.Data.Email)
if err != nil {
exception := 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,
}
return
}
externalUrl := viper.GetString("server.external_url")
url, err := url.Parse(externalUrl)
if err != nil {
exception := 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,
}
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)
url.RawQuery = query.Encode()
debugMode := viper.GetBool("server.debug_mode")
if debugMode {
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},
}
return
} else {
emailClient, err := new(email.Client).NewSMTPClient()
if err != nil {
exception := 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,
}
return
}
emailClient.Send(
"NixCN CMS <cms@yuri.nix.org.cn>",
payload.Data.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).
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
}

View File

@@ -0,0 +1,210 @@
package service_auth
import (
"context"
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/authcode"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"github.com/google/uuid"
"gorm.io/gorm"
)
type RedirectData struct {
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Code string `json:"code"`
}
type RedirectPayload struct {
Context context.Context
Data *RedirectData
}
type RedirectResult struct {
Common shared.CommonResult
Data string
}
func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *RedirectResult) {
var err error
authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code)
if !ok {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectTokenInvalid).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 403,
Exception: exception,
},
}
return
}
userData, err := new(data.User).
GetByEmail(payload.Context, &authCode.Email)
if err != nil {
if err == gorm.ErrRecordNotFound {
userData.UUID = uuid.New()
userData.UserId = uuid.New()
userData.Email = authCode.Email
userData.Username = userData.UserId.String()
userData.PermissionLevel = 10
if err := userData.Create(payload.Context); err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
}
return
}
} else {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
}
return
}
}
clientData := new(data.Client)
client, err := clientData.GetClientByClientId(payload.Context, payload.Data.ClientId)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectClientNotFound).
SetError(err).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exception,
},
}
return
}
if err = client.ValidateRedirectURI(payload.Data.RedirectUri); err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectUriMismatch).
SetError(err).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exception,
},
}
return
}
newCode, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, authCode.Email)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
}
return
}
targetUrl, err := url.Parse(payload.Data.RedirectUri)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRedirectInvalidUri).
SetError(err).
Throw(payload.Context)
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exception,
},
}
return
}
query := targetUrl.Query()
query.Set("code", newCode)
if payload.Data.State != "" {
query.Set("state", payload.Data.State)
}
targetUrl.RawQuery = query.Encode()
result = &RedirectResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRedirect).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
Data: targetUrl.String(),
}
return
}

View File

@@ -0,0 +1,99 @@
package service_auth
import (
"context"
"nixcn-cms/internal/authtoken"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"github.com/spf13/viper"
)
type RefreshData struct {
RefreshToken string `json:"refresh_token"`
}
type RefreshPayload struct {
Context context.Context
Data *RefreshData
}
type RefreshResult struct {
Common shared.CommonResult
Data *TokenResponse
}
func (self *AuthServiceImpl) Refresh(payload *RefreshPayload) (result *RefreshResult) {
JwtTool := authtoken.Token{
Application: viper.GetString("server.application"),
}
// 1. Refresh Access Token
accessToken, err := JwtTool.RefreshAccessToken(payload.Context, payload.Data.RefreshToken)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRefreshInvalidToken).
SetError(err).
Throw(payload.Context)
result = &RefreshResult{
Common: shared.CommonResult{
HttpCode: 401,
Exception: exception,
},
Data: nil,
}
return
}
// 2. Renew Refresh Token (Rotation)
refreshToken, err := JwtTool.RenewRefreshToken(payload.Context, payload.Data.RefreshToken)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthRefreshRenewFailed).
SetError(err).
Throw(payload.Context)
result = &RefreshResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
// 3. Success Assignment
exception := new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceRefresh).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &RefreshResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: &TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
},
}
return
}

View File

@@ -0,0 +1,15 @@
package service_auth
type AuthService interface {
Exchange(*ExchangePayload) *ExchangeResult
Magic(*MagicPayload) *MagicResult
Redirect(*RedirectPayload) *RedirectResult
Token(*TokenPayload) *TokenResult
Refresh(*RefreshPayload) *RefreshResult
}
type AuthServiceImpl struct{}
func NewAuthService() AuthService {
return &AuthServiceImpl{}
}

View File

@@ -0,0 +1,118 @@
package service_auth
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/authcode"
"nixcn-cms/internal/authtoken"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"github.com/spf13/viper"
)
type TokenData struct {
Code string `json:"code"`
}
type TokenPayload struct {
Context context.Context
Data *TokenData
}
type TokenResult struct {
Common shared.CommonResult
Data *TokenResponse
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func (self *AuthServiceImpl) Token(payload *TokenPayload) (result *TokenResult) {
authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code)
if !ok {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthTokenInvalidToken).
Throw(payload.Context)
result = &TokenResult{
Common: shared.CommonResult{
HttpCode: 403,
Exception: exception,
},
}
return
}
userData := new(data.User)
user, err := userData.GetByEmail(payload.Context, &authCode.Email)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInternal).
SetError(err).
Throw(payload.Context)
result = &TokenResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
}
return
}
JwtTool := authtoken.Token{
Application: viper.GetString("server.application"),
}
accessToken, refreshToken, err := JwtTool.IssueTokens(payload.Context, authCode.ClientId, user.UserId)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeSpecific).
SetOriginal(exception.AuthTokenGenFailed).
SetError(err).
Throw(payload.Context)
result = &TokenResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
}
return
}
result = &TokenResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceAuth).
SetEndpoint(exception.EndpointAuthServiceToken).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
Data: &TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
},
}
return
}

View File

@@ -0,0 +1,193 @@
package service_event
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"time"
"github.com/google/uuid"
)
type CheckinData struct {
EventId uuid.UUID `json:"event_id"`
}
type CheckinPayload struct {
Context context.Context
UserId uuid.UUID
Data *CheckinData
}
type CheckinResponse struct {
CheckinCode *string `json:"checkin_code"`
}
type CheckinResult struct {
Common shared.CommonResult
Data *CheckinResponse
}
func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) {
attendance := &data.Attendance{UserId: payload.UserId}
code, err := attendance.GenCheckinCode(payload.Context, payload.Data.EventId)
if err != nil {
result = &CheckinResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventCheckinGenCodeFailed).
SetError(err).
Throw(payload.Context),
},
}
return
}
result = &CheckinResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckin).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
Data: &CheckinResponse{code},
}
return
}
type CheckinSubmitData struct {
CheckinCode string `json:"checkin_code"`
}
type CheckinSubmitPayload struct {
Context context.Context
Data *CheckinSubmitData
}
type CheckinSubmitResult struct {
Common shared.CommonResult
}
func (self *EventServiceImpl) CheckinSubmit(payload *CheckinSubmitPayload) (result *CheckinSubmitResult) {
attendanceData := new(data.Attendance)
err := attendanceData.VerifyCheckinCode(payload.Context, payload.Data.CheckinCode)
if err != nil {
result = &CheckinSubmitResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context),
},
}
return
}
result = &CheckinSubmitResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
}
return
}
type CheckinQueryData struct {
EventId uuid.UUID `json:"event_id"`
}
type CheckinQueryPayload struct {
Context context.Context
UserId uuid.UUID
Data *CheckinQueryData
}
type CheckinQueryResponse struct {
CheckinAt *time.Time `json:"checkin_at"`
}
type CheckinQueryResult struct {
Common shared.CommonResult
Data *CheckinQueryResponse
}
func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result *CheckinQueryResult) {
attendanceData := new(data.Attendance)
attendance, err := attendanceData.GetAttendance(payload.Context, payload.UserId, payload.Data.EventId)
if err != nil {
result = &CheckinQueryResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Throw(payload.Context),
},
}
return
}
if attendance == nil {
result = &CheckinQueryResult{
Common: shared.CommonResult{
HttpCode: 404,
Exception: new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventCheckinQueryRecordNotFound).
Throw(payload.Context),
},
}
return
}
var checkinAt *time.Time
if !attendance.CheckinAt.IsZero() {
checkinAt = &attendance.CheckinAt
}
result = &CheckinQueryResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
Data: &CheckinQueryResponse{checkinAt},
}
return
}

View File

@@ -0,0 +1,74 @@
package service_event
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"time"
"github.com/google/uuid"
)
type InfoData struct {
EventId uuid.UUID `json:"event_id"`
}
type InfoPayload struct {
Context context.Context
Data *InfoData
}
type InfoResponse struct {
Name string `json:"name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
type InfoResult struct {
Common shared.CommonResult
Data *InfoResponse
}
func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) {
event, err := new(data.Event).GetEventById(payload.Context, payload.Data.EventId)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeSpecific).
SetOriginal(exception.EventInfoNotFound).
SetError(err).
Throw(payload.Context)
result = &InfoResult{
Common: shared.CommonResult{
HttpCode: 404,
Exception: exception,
},
}
return
}
result = &InfoResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: new(exception.Builder).
SetStatus(exception.StatusSuccess).
SetService(exception.ServiceEvent).
SetEndpoint(exception.EndpointEventServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
Throw(payload.Context),
},
Data: &InfoResponse{
Name: event.Name,
StartTime: event.StartTime,
EndTime: event.EndTime,
},
}
return
}

View File

@@ -0,0 +1,14 @@
package service_event
type EventService interface {
Checkin(*CheckinPayload) *CheckinResult
CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult
CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult
Info(*InfoPayload) *InfoResult
}
type EventServiceImpl struct{}
func NewEventService() EventService {
return &EventServiceImpl{}
}

View File

@@ -0,0 +1,3 @@
package service_user
func (self *UserServiceImpl) CreateUser() {}

View File

@@ -0,0 +1,94 @@
package service_user
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"github.com/google/uuid"
)
type UserInfoData struct {
UserId uuid.UUID `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
PermissionLevel uint `json:"permission_level"`
AllowPublic bool `json:"allow_public"`
}
type UserInfoPayload struct {
Context context.Context
UserId uuid.UUID
Data *UserInfoData
}
type UserInfoResult struct {
Common shared.CommonResult
Data *UserInfoData
}
// GetUserInfo
func (self *UserServiceImpl) GetUserInfo(payload *UserInfoPayload) (result *UserInfoResult) {
var err error
userData, err := new(data.User).
GetByUserId(
payload.Context,
&payload.UserId,
)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 404,
Exception: exception,
},
Data: nil,
}
return
}
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: &UserInfoData{
UserId: userData.UserId,
Email: userData.Email,
Username: userData.Username,
Nickname: userData.Nickname,
Subtitle: userData.Subtitle,
Avatar: userData.Avatar,
Bio: userData.Bio,
PermissionLevel: userData.PermissionLevel,
AllowPublic: userData.AllowPublic,
},
}
return
}

View File

@@ -0,0 +1,69 @@
package service_user
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
)
type UserTablePayload struct {
Context context.Context
}
type UserTableResponse struct {
UserTable *[]data.User `json:"user_table"`
}
type UserTableResult struct {
Common shared.CommonResult
Data *UserTableResponse
}
// ListUserFullTable
func (self *UserServiceImpl) GetUserFullTable(payload *UserTablePayload) (result *UserTableResult) {
var err error
userFullTable, err := new(data.User).
GetFullTable(payload.Context)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Throw(payload.Context)
result = &UserTableResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserTableResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: &UserTableResponse{userFullTable},
}
return
}

View File

@@ -0,0 +1,138 @@
package service_user
import (
"context"
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"strconv"
)
type UserListPayload struct {
Context context.Context
Limit *string
Offset *string
}
type UserListResult struct {
Common shared.CommonResult
Data *[]data.UserSearchDoc `json:"user_list"`
}
func (self *UserServiceImpl) ListUsers(payload *UserListPayload) (result *UserListResult) {
var limit string
if payload.Limit == nil || *payload.Limit == "" {
limit = "0"
}
var offset string
if payload.Offset == nil || *payload.Offset == "" {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(payload.Context)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
} else {
offset = *payload.Offset
}
// Parse string to int64
limitNum, err := strconv.ParseInt(limit, 10, 64)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exception,
},
Data: nil,
}
return
}
offsetNum, err := strconv.ParseInt(offset, 10, 64)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: exception,
},
Data: nil,
}
return
}
// Get user list from search engine
userList, err := new(data.User).
FastListUsers(payload.Context, &limitNum, &offsetNum)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeSpecific).
SetOriginal(exception.UserListMeilisearchFailed).
SetError(err).
Throw(payload.Context)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
}
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserListResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: userList,
}
return
}

View File

@@ -0,0 +1,15 @@
package service_user
type UserService interface {
GetUserInfo(*UserInfoPayload) *UserInfoResult
UpdateUserInfo(*UserInfoPayload) *UserInfoResult
ListUsers(*UserListPayload) *UserListResult
GetUserFullTable(*UserTablePayload) *UserTableResult
CreateUser()
}
type UserServiceImpl struct{}
func NewUserService() UserService {
return &UserServiceImpl{}
}

View File

@@ -0,0 +1,177 @@
package service_user
import (
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"nixcn-cms/internal/exception"
"nixcn-cms/service/shared"
"unicode/utf8"
)
func (self *UserServiceImpl) UpdateUserInfo(payload *UserInfoPayload) (result *UserInfoResult) {
var err error
userData := new(data.User).
SetNickname(payload.Data.Nickname).
SetSubtitle(payload.Data.Subtitle).
SetAvatar(payload.Data.Avatar).
SetBio(payload.Data.Bio).
SetAllowPublic(payload.Data.AllowPublic)
if payload.Data.Username != "" {
if len(payload.Data.Username) < 5 || len(payload.Data.Username) >= 255 {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetUsername(payload.Data.Username)
}
if payload.Data.Nickname != "" {
if utf8.RuneCountInString(payload.Data.Nickname) > 24 {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetNickname(payload.Data.Nickname)
}
if payload.Data.Subtitle != "" {
if utf8.RuneCountInString(payload.Data.Subtitle) > 32 {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetSubtitle(payload.Data.Subtitle)
}
if payload.Data.Avatar != "" {
_, err := url.ParseRequestURI(payload.Data.Avatar)
if err != nil {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.SetAvatar(payload.Data.Avatar)
}
if payload.Data.Bio != "" {
if !cryptography.IsBase64Std(payload.Data.Bio) {
execption := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 400,
Exception: execption,
},
Data: nil,
}
return
}
userData.Bio = payload.Data.Bio
}
err = userData.UpdateByUserID(payload.Context, &payload.UserId)
if err != nil {
exception := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 500,
Exception: exception,
},
Data: nil,
}
return
}
exception := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonSuccess).
SetError(nil).
Throw(payload.Context)
result = &UserInfoResult{
Common: shared.CommonResult{
HttpCode: 200,
Exception: exception,
},
Data: nil,
}
return
}

8
service/shared/common.go Normal file
View File

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

View File

@@ -1,71 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Full(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Build()
utils.HttpResponse(c, 404, errorCode)
return
}
users, err := userData.GetFullTable()
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceFull).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorDatabase).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
userFullResp := struct {
UserTable *[]data.User `json:"user_table"`
}{users}
utils.HttpResponse(c, 200, "", "success", userFullResp)
}

View File

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

View File

@@ -1,67 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Info(c *gin.Context) {
userData := new(data.User)
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
// Get user from database
user, err := userData.GetByUserId(userId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceInfo).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Build()
utils.HttpResponse(c, 404, errorCode)
return
}
userInfoResp := struct {
UserId uuid.UUID `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
PermissionLevel uint `json:"permission_level"`
}{user.UserId, user.Email, user.Username, user.Nickname, user.Subtitle, user.Avatar, user.Bio, user.PermissionLevel}
utils.HttpResponse(c, 200, "", "success", userInfoResp)
}

View File

@@ -1,77 +0,0 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"strconv"
"github.com/gin-gonic/gin"
)
func List(c *gin.Context) {
// Get limit and offset from query
limit, ok := c.GetQuery("limit")
if !ok {
limit = "0"
}
offset, ok := c.GetQuery("offset")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
// Parse string to int64
limitNum, err := strconv.ParseInt(limit, 10, 64)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
offsetNum, err := strconv.ParseInt(offset, 10, 64)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
// Get user list from search engine
list, err := new(data.User).FastListUsers(limitNum, offsetNum)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceList).
SetType(exception.TypeSpecific).
SetOriginal(exception.UserListMeilisearchFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
}
userListResp := struct {
List *[]data.UserSearchDoc `json:"list"`
}{list}
utils.HttpResponse(c, 200, "", "success", userListResp)
}

View File

@@ -1,163 +0,0 @@
package user
import (
"net/url"
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"nixcn-cms/internal/exception"
"nixcn-cms/utils"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Update(c *gin.Context) {
// New user model
userIdOrig, ok := c.Get("user_id")
if !ok {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorMissingUserId).
Build()
utils.HttpResponse(c, 403, errorCode)
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusServer).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUuidParseFailed).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
var ReqInfo data.User
err = c.ShouldBindJSON(&ReqInfo)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
// Get user info
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusUser).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorUserNotFound).
SetError(err).
Build()
utils.HttpResponse(c, 500, errorCode)
return
}
// if len(ReqInfo.Email) < 5 || len(ReqInfo.Email) >= 255 {
// utils.HttpResponse(c, 400, "", "invilad email")
// return
// }
// userData.Email = ReqInfo.Email
// utils.HttpResponse(c, 400, "", "invilad user name")
// return
if ReqInfo.Username != "" {
if len(ReqInfo.Username) < 5 || len(ReqInfo.Username) >= 255 {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userData.Username = ReqInfo.Username
}
if ReqInfo.Nickname != "" {
if utf8.RuneCountInString(ReqInfo.Nickname) > 24 {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userData.Nickname = ReqInfo.Nickname
}
if ReqInfo.Subtitle != "" {
if utf8.RuneCountInString(ReqInfo.Subtitle) > 32 {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userData.Subtitle = ReqInfo.Subtitle
}
if ReqInfo.Avatar != "" {
_, err := url.ParseRequestURI(ReqInfo.Avatar)
if err != nil {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
SetError(err).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userData.Avatar = ReqInfo.Avatar
}
if ReqInfo.Bio != "" {
if !cryptography.IsBase64Std(ReqInfo.Bio) {
errorCode := new(exception.Builder).
SetStatus(exception.StatusClient).
SetService(exception.ServiceUser).
SetEndpoint(exception.EndpointUserServiceUpdate).
SetType(exception.TypeCommon).
SetOriginal(exception.CommonErrorInvalidInput).
Build()
utils.HttpResponse(c, 400, errorCode)
return
}
userData.Bio = ReqInfo.Bio
}
// Update user info
userData.UpdateByUserID(userId)
utils.HttpResponse(c, 200, "", "success")
}

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