diff --git a/api/auth/exchange.go b/api/auth/exchange.go index e69de29..73cc4fe 100644 --- a/api/auth/exchange.go +++ b/api/auth/exchange.go @@ -0,0 +1,87 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Exchange handles the authorization code swap process. +// @Summary Exchange Auth Code +// @Description Exchanges client credentials and user session for a specific redirect authorization code. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials" +// @Success 200 {object} service_auth.ExchangeResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal Server Error" +// @Security ApiKeyAuth +// @Router /auth/exchange [post] +func (self *AuthHandler) Exchange(c *gin.Context) { + var exchangeData service_auth.ExchangeData + + if err := c.ShouldBindJSON(&exchangeData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + userIdOrig, ok := c.Get("user_id") + if !ok { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUnauthorized). + SetError(nil). + Throw(c). + String() + + utils.HttpResponse(c, 401, errorCode) + return + } + + userId, err := uuid.Parse(userIdOrig.(string)) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceExchange). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 500, errorCode) + return + } + + result := self.svc.Exchange(&service_auth.ExchangePayload{ + Context: c, + UserId: userId, + Data: &exchangeData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/auth/handler.go b/api/auth/handler.go index ad630b1..a920165 100644 --- a/api/auth/handler.go +++ b/api/auth/handler.go @@ -1,8 +1,23 @@ package auth import ( + "nixcn-cms/middleware" + "nixcn-cms/service/service_auth" + "github.com/gin-gonic/gin" ) -func ApiHandler(r *gin.RouterGroup) { +type AuthHandler struct { + svc service_auth.AuthService +} + +func ApiHandler(r *gin.RouterGroup) { + authSvc := service_auth.NewAuthService() + authHandler := &AuthHandler{authSvc} + + r.GET("/redirect", authHandler.Redirect) + r.POST("/magic", middleware.ApiVersionCheck(), authHandler.Magic) + r.POST("/token", middleware.ApiVersionCheck(), authHandler.Token) + r.POST("/refresh", middleware.ApiVersionCheck(), authHandler.Refresh) + r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), authHandler.Exchange) } diff --git a/api/auth/magic.go b/api/auth/magic.go index e69de29..11c3528 100644 --- a/api/auth/magic.go +++ b/api/auth/magic.go @@ -0,0 +1,54 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Magic handles the "Magic Link" authentication request. +// @Summary Request Magic Link +// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.MagicData true "Magic Link Request Data" +// @Success 200 {object} service_auth.MagicResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 403 {string} string "Turnstile Verification Failed" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/magic [post] +func (self *AuthHandler) Magic(c *gin.Context) { + var magicData service_auth.MagicData + + if err := c.ShouldBindJSON(&magicData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceMagic). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + magicData.ClientIP = c.ClientIP() + + result := self.svc.Magic(&service_auth.MagicPayload{ + Context: c, + Data: &magicData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/auth/redirect.go b/api/auth/redirect.go index e69de29..8e2742b 100644 --- a/api/auth/redirect.go +++ b/api/auth/redirect.go @@ -0,0 +1,59 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Redirect handles the post-verification callback and redirects the user to the target application. +// @Summary Handle Auth Callback and Redirect +// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code. +// @Tags Authentication +// @Accept x-www-form-urlencoded +// @Produce html +// @Param client_id query string true "Client Identifier" +// @Param redirect_uri query string true "Target Redirect URI" +// @Param code query string true "Temporary Verification Code" +// @Param state query string false "Opaque state used to maintain state between the request and callback" +// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code" +// @Failure 400 {string} string "Invalid Input / Client Not Found / URI Mismatch" +// @Failure 403 {string} string "Invalid or Expired Verification Code" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/redirect [get] +func (self *AuthHandler) Redirect(c *gin.Context) { + data := &service_auth.RedirectData{ + ClientId: c.Query("client_id"), + RedirectUri: c.Query("redirect_uri"), + State: c.Query("state"), + Code: c.Query("code"), + } + + if data.ClientId == "" || data.RedirectUri == "" || data.Code == "" { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(nil). + Throw(c). + String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Redirect(&service_auth.RedirectPayload{ + Context: c, + Data: data, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + c.Redirect(302, result.Data) +} diff --git a/api/auth/refresh.go b/api/auth/refresh.go index e69de29..d6ee131 100644 --- a/api/auth/refresh.go +++ b/api/auth/refresh.go @@ -0,0 +1,52 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Refresh handles the token rotation process. +// @Summary Refresh Access Token +// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token. +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.RefreshData true "Refresh Token Body" +// @Success 200 {object} service_auth.RefreshResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 401 {string} string "Invalid Refresh Token" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/refresh [post] +func (self *AuthHandler) Refresh(c *gin.Context) { + var refreshData service_auth.RefreshData + + if err := c.ShouldBindJSON(&refreshData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Refresh(&service_auth.RefreshPayload{ + Context: c, + Data: &refreshData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/auth/token.go b/api/auth/token.go index e69de29..942f322 100644 --- a/api/auth/token.go +++ b/api/auth/token.go @@ -0,0 +1,52 @@ +package auth + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_auth" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// Token exchanges an authorization code for access and refresh tokens. +// @Summary Exchange Code for Token +// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh). +// @Tags Authentication +// @Accept json +// @Produce json +// @Param payload body service_auth.TokenData true "Token Request Body" +// @Success 200 {object} service_auth.TokenResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 403 {string} string "Invalid or Expired Code" +// @Failure 500 {string} string "Internal Server Error" +// @Router /auth/token [post] +func (self *AuthHandler) Token(c *gin.Context) { + var tokenData service_auth.TokenData + + if err := c.ShouldBindJSON(&tokenData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Token(&service_auth.TokenPayload{ + Context: c, + Data: &tokenData, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/event/checkin.go b/api/event/checkin.go new file mode 100644 index 0000000..f980662 --- /dev/null +++ b/api/event/checkin.go @@ -0,0 +1,113 @@ +package event + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_event" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Checkin generates a check-in code for a specific event. +// @Summary Generate Check-in Code +// @Description Creates a temporary check-in code for the authenticated user and event. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} service_event.CheckinResult +// @Router /event/checkin [get] +func (self *EventHandler) Checkin(c *gin.Context) { + userIdOrig, _ := c.Get("user_id") + userId, _ := uuid.Parse(userIdOrig.(string)) + + eventIdOrig := c.Query("event_id") + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c).String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.Checkin(&service_event.CheckinPayload{ + Context: c, + UserId: userId, + Data: &service_event.CheckinData{EventId: eventId}, + }) + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data) +} + +// CheckinSubmit validates a check-in code to complete attendance. +// @Summary Submit Check-in Code +// @Description Submits the generated code to mark the user as attended. +// @Tags Event +// @Accept json +// @Produce json +// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data" +// @Success 200 {object} service_event.CheckinSubmitResult +// @Router /event/checkin/submit [post] +func (self *EventHandler) CheckinSubmit(c *gin.Context) { + var data service_event.CheckinSubmitData + if err := c.ShouldBindJSON(&data); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinSubmit). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c).String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.CheckinSubmit(&service_event.CheckinSubmitPayload{ + Context: c, + Data: &data, + }) + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) +} + +// CheckinQuery retrieves the check-in status of a user for an event. +// @Summary Query Check-in Status +// @Description Returns the timestamp of when the user checked in, or null if not yet checked in. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} service_event.CheckinQueryResult +// @Router /event/checkin/query [get] +func (self *EventHandler) CheckinQuery(c *gin.Context) { + userIdOrig, _ := c.Get("user_id") + userId, _ := uuid.Parse(userIdOrig.(string)) + + eventIdOrig := c.Query("event_id") + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c).String() + utils.HttpResponse(c, 400, errorCode) + return + } + + result := self.svc.CheckinQuery(&service_event.CheckinQueryPayload{ + Context: c, + UserId: userId, + Data: &service_event.CheckinQueryData{EventId: eventId}, + }) + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data) +} diff --git a/api/event/handler.go b/api/event/handler.go index 8e182c5..0746218 100644 --- a/api/event/handler.go +++ b/api/event/handler.go @@ -2,10 +2,22 @@ package event import ( "nixcn-cms/middleware" + "nixcn-cms/service/service_event" "github.com/gin-gonic/gin" ) -func ApiHandler(r *gin.RouterGroup) { - r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10)) +type EventHandler struct { + svc service_event.EventService +} + +func ApiHandler(r *gin.RouterGroup) { + eventSvc := service_event.NewEventService() + eventHandler := &EventHandler{eventSvc} + + r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10)) + r.GET("/info", eventHandler.Info) + r.GET("/checkin", eventHandler.Checkin) + r.GET("/checkin/query", eventHandler.CheckinQuery) + r.POST("/checkin/submit", middleware.Permission(20), eventHandler.CheckinSubmit) } diff --git a/api/event/info.go b/api/event/info.go new file mode 100644 index 0000000..0695d3a --- /dev/null +++ b/api/event/info.go @@ -0,0 +1,55 @@ +package event + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_event" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// Info retrieves basic information about a specific event. +// @Summary Get Event Information +// @Description Fetches the name, start time, and end time of an event using its UUID. +// @Tags Event +// @Accept json +// @Produce json +// @Param event_id query string true "Event UUID" +// @Success 200 {object} service_event.InfoResult +// @Failure 400 {string} string "Invalid Input" +// @Failure 404 {string} string "Event Not Found" +// @Failure 500 {string} string "Internal Server Error" +// @Router /event/info [get] +func (self *EventHandler) Info(c *gin.Context) { + eventIdOrig := c.Query("event_id") + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceInfo). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(c). + String() + + utils.HttpResponse(c, 500, errorCode) + return + } + + result := self.svc.Info(&service_event.InfoPayload{ + Context: c, + Data: &service_event.InfoData{ + EventId: eventId, + }, + }) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data) +} diff --git a/api/user/full.go b/api/user/full.go index a89c6cc..c5a2440 100644 --- a/api/user/full.go +++ b/api/user/full.go @@ -8,6 +8,16 @@ import ( "github.com/gin-gonic/gin" ) +// Full retrieves the complete list of users directly from the database table. +// @Summary Get Full User Table +// @Description Fetches all user records without pagination. This is typically used for administrative overview or data export. +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} service_user.UserTableResult +// @Failure 500 {string} string "Internal Server Error (Database Error)" +// @Security ApiKeyAuth +// @Router /user/full [get] func (self *UserHandler) Full(c *gin.Context) { userTablePayload := &service_user.UserTablePayload{ Context: c, diff --git a/api/user/info.go b/api/user/info.go index bb8cc0a..bcf21af 100644 --- a/api/user/info.go +++ b/api/user/info.go @@ -9,6 +9,18 @@ import ( "github.com/google/uuid" ) +// Info retrieves the profile information of the currently authenticated user. +// @Summary Get My User Information +// @Description Fetches the complete profile data for the user associated with the provided session/token. +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} service_user.UserInfoResult +// @Failure 403 {string} string "Missing User ID / Unauthorized" +// @Failure 404 {string} string "User Not Found" +// @Failure 500 {string} string "Internal Server Error (UUID Parse Failed)" +// @Security ApiKeyAuth +// @Router /user/info [get] func (self *UserHandler) Info(c *gin.Context) { userIdOrig, ok := c.Get("user_id") if !ok { diff --git a/api/user/list.go b/api/user/list.go index f4fce52..48c9934 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -8,6 +8,18 @@ import ( "github.com/gin-gonic/gin" ) +// List retrieves a paginated list of users from the search engine. +// @Summary List Users +// @Description Fetches a list of users with support for pagination via limit and offset. +// @Tags User +// @Accept json +// @Produce json +// @Param limit query string false "Maximum number of users to return (default 0)" +// @Param offset query string true "Number of users to skip" +// @Success 200 {object} service_user.UserListResult +// @Failure 400 {string} string "Invalid Input (Format Error)" +// @Failure 500 {string} string "Internal Server Error (Search Engine or Missing Offset)" +// @Router /user/list [get] func (self *UserHandler) List(c *gin.Context) { type ListQuery struct { Limit *string `form:"limit"` diff --git a/api/user/update.go b/api/user/update.go index 9382774..ec34c1a 100644 --- a/api/user/update.go +++ b/api/user/update.go @@ -9,6 +9,20 @@ import ( "github.com/google/uuid" ) +// Update modifies the profile information for the currently authenticated user. +// @Summary Update User Information +// @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). +// @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). +// @Tags User +// @Accept json +// @Produce json +// @Param payload body service_user.UserInfoData true "Updated User Profile Data" +// @Success 200 {object} service_user.UserInfoResult +// @Failure 400 {string} string "Invalid Input (Validation Failed)" +// @Failure 403 {string} string "Missing User ID / Unauthorized" +// @Failure 500 {string} string "Internal Server Error (Database Error / UUID Parse Failed)" +// @Security ApiKeyAuth +// @Router /user/update [patch] func (self *UserHandler) Update(c *gin.Context) { userIdOrig, ok := c.Get("user_id") if !ok { diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..c235495 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1008 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/auth/exchange": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Exchanges client credentials and user session for a specific redirect authorization code.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Auth Code", + "parameters": [ + { + "description": "Exchange Request Credentials", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.ExchangeData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.ExchangeResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/magic": { + "post": { + "description": "Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Request Magic Link", + "parameters": [ + { + "description": "Magic Link Request Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.MagicData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.MagicResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Turnstile Verification Failed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/redirect": { + "get": { + "description": "Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "text/html" + ], + "tags": [ + "Authentication" + ], + "summary": "Handle Auth Callback and Redirect", + "parameters": [ + { + "type": "string", + "description": "Client Identifier", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Target Redirect URI", + "name": "redirect_uri", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Temporary Verification Code", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Opaque state used to maintain state between the request and callback", + "name": "state", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Redirect to the provided RedirectUri with a new code", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid Input / Client Not Found / URI Mismatch", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Verification Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Accepts a valid refresh token to issue a new access token and a rotated refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh Access Token", + "parameters": [ + { + "description": "Refresh Token Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.RefreshData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.RefreshResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Invalid Refresh Token", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/token": { + "post": { + "description": "Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Code for Token", + "parameters": [ + { + "description": "Token Request Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.TokenData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.TokenResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/event/checkin": { + "get": { + "description": "Creates a temporary check-in code for the authenticated user and event.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Generate Check-in Code", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinResult" + } + } + } + } + }, + "/event/checkin/query": { + "get": { + "description": "Returns the timestamp of when the user checked in, or null if not yet checked in.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Query Check-in Status", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinQueryResult" + } + } + } + } + }, + "/event/checkin/submit": { + "post": { + "description": "Submits the generated code to mark the user as attended.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Submit Check-in Code", + "parameters": [ + { + "description": "Checkin Code Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitResult" + } + } + } + } + }, + "/event/info": { + "get": { + "description": "Fetches the name, start time, and end time of an event using its UUID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Get Event Information", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.InfoResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Event Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/full": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches all user records without pagination. This is typically used for administrative overview or data export.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get Full User Table", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserTableResult" + } + }, + "500": { + "description": "Internal Server Error (Database Error)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the complete profile data for the user associated with the provided session/token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get My User Information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "User Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/list": { + "get": { + "description": "Fetches a list of users with support for pagination via limit and offset.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List Users", + "parameters": [ + { + "type": "string", + "description": "Maximum number of users to return (default 0)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Number of users to skip", + "name": "offset", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserListResult" + } + }, + "400": { + "description": "Invalid Input (Format Error)", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Search Engine or Missing Offset)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/update": { + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).\nValidation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update User Information", + "parameters": [ + { + "description": "Updated User Profile Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "400": { + "description": "Invalid Input (Validation Failed)", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Database Error / UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "data.User": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "data.UserSearchDoc": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "exception.Builder": { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + }, + "error": {}, + "errorCode": { + "type": "string" + }, + "original": { + "type": "string" + }, + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service_auth.ExchangeData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "service_auth.ExchangeResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "redirect_uri": { + "type": "string" + } + } + } + } + }, + "service_auth.MagicData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "email": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + }, + "turnstile_token": { + "type": "string" + } + } + }, + "service_auth.MagicResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": {} + } + }, + "service_auth.RefreshData": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.RefreshResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_auth.TokenData": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "service_auth.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.TokenResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_event.CheckinQueryResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_at": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinSubmitData": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + }, + "service_event.CheckinSubmitResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + } + } + }, + "service_event.InfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "end_time": { + "type": "string" + }, + "name": { + "type": "string" + }, + "start_time": { + "type": "string" + } + } + } + } + }, + "service_user.UserInfoData": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service_user.UserInfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + }, + "service_user.UserListResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_list": { + "type": "array", + "items": { + "$ref": "#/definitions/data.UserSearchDoc" + } + } + } + }, + "service_user.UserTableResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_table": { + "type": "array", + "items": { + "$ref": "#/definitions/data.User" + } + } + } + }, + "shared.CommonResult": { + "type": "object", + "properties": { + "exception": { + "$ref": "#/definitions/exception.Builder" + }, + "httpCode": { + "type": "integer" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..d3e5f8d --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,979 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/auth/exchange": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Exchanges client credentials and user session for a specific redirect authorization code.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Auth Code", + "parameters": [ + { + "description": "Exchange Request Credentials", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.ExchangeData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.ExchangeResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/magic": { + "post": { + "description": "Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Request Magic Link", + "parameters": [ + { + "description": "Magic Link Request Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.MagicData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.MagicResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Turnstile Verification Failed", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/redirect": { + "get": { + "description": "Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "text/html" + ], + "tags": [ + "Authentication" + ], + "summary": "Handle Auth Callback and Redirect", + "parameters": [ + { + "type": "string", + "description": "Client Identifier", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Target Redirect URI", + "name": "redirect_uri", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Temporary Verification Code", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Opaque state used to maintain state between the request and callback", + "name": "state", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Redirect to the provided RedirectUri with a new code", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid Input / Client Not Found / URI Mismatch", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Verification Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Accepts a valid refresh token to issue a new access token and a rotated refresh token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh Access Token", + "parameters": [ + { + "description": "Refresh Token Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.RefreshData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.RefreshResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Invalid Refresh Token", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/auth/token": { + "post": { + "description": "Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Exchange Code for Token", + "parameters": [ + { + "description": "Token Request Body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_auth.TokenData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_auth.TokenResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Invalid or Expired Code", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/event/checkin": { + "get": { + "description": "Creates a temporary check-in code for the authenticated user and event.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Generate Check-in Code", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinResult" + } + } + } + } + }, + "/event/checkin/query": { + "get": { + "description": "Returns the timestamp of when the user checked in, or null if not yet checked in.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Query Check-in Status", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinQueryResult" + } + } + } + } + }, + "/event/checkin/submit": { + "post": { + "description": "Submits the generated code to mark the user as attended.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Submit Check-in Code", + "parameters": [ + { + "description": "Checkin Code Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.CheckinSubmitResult" + } + } + } + } + }, + "/event/info": { + "get": { + "description": "Fetches the name, start time, and end time of an event using its UUID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Get Event Information", + "parameters": [ + { + "type": "string", + "description": "Event UUID", + "name": "event_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_event.InfoResult" + } + }, + "400": { + "description": "Invalid Input", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Event Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/full": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches all user records without pagination. This is typically used for administrative overview or data export.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get Full User Table", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserTableResult" + } + }, + "500": { + "description": "Internal Server Error (Database Error)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the complete profile data for the user associated with the provided session/token.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get My User Information", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "User Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/list": { + "get": { + "description": "Fetches a list of users with support for pagination via limit and offset.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List Users", + "parameters": [ + { + "type": "string", + "description": "Maximum number of users to return (default 0)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Number of users to skip", + "name": "offset", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserListResult" + } + }, + "400": { + "description": "Invalid Input (Format Error)", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Search Engine or Missing Offset)", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/update": { + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).\nValidation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update User Information", + "parameters": [ + { + "description": "Updated User Profile Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service_user.UserInfoResult" + } + }, + "400": { + "description": "Invalid Input (Validation Failed)", + "schema": { + "type": "string" + } + }, + "403": { + "description": "Missing User ID / Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error (Database Error / UUID Parse Failed)", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "data.User": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "data.UserSearchDoc": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "exception.Builder": { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + }, + "error": {}, + "errorCode": { + "type": "string" + }, + "original": { + "type": "string" + }, + "service": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service_auth.ExchangeData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + } + } + }, + "service_auth.ExchangeResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "redirect_uri": { + "type": "string" + } + } + } + } + }, + "service_auth.MagicData": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "email": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "state": { + "type": "string" + }, + "turnstile_token": { + "type": "string" + } + } + }, + "service_auth.MagicResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": {} + } + }, + "service_auth.RefreshData": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.RefreshResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_auth.TokenData": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "service_auth.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "service_auth.TokenResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_auth.TokenResponse" + } + } + }, + "service_event.CheckinQueryResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_at": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + } + } + }, + "service_event.CheckinSubmitData": { + "type": "object", + "properties": { + "checkin_code": { + "type": "string" + } + } + }, + "service_event.CheckinSubmitResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + } + } + }, + "service_event.InfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "type": "object", + "properties": { + "end_time": { + "type": "string" + }, + "name": { + "type": "string" + }, + "start_time": { + "type": "string" + } + } + } + } + }, + "service_user.UserInfoData": { + "type": "object", + "properties": { + "allow_public": { + "type": "boolean" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "permission_level": { + "type": "integer" + }, + "subtitle": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service_user.UserInfoResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "data": { + "$ref": "#/definitions/service_user.UserInfoData" + } + } + }, + "service_user.UserListResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_list": { + "type": "array", + "items": { + "$ref": "#/definitions/data.UserSearchDoc" + } + } + } + }, + "service_user.UserTableResult": { + "type": "object", + "properties": { + "common": { + "$ref": "#/definitions/shared.CommonResult" + }, + "user_table": { + "type": "array", + "items": { + "$ref": "#/definitions/data.User" + } + } + } + }, + "shared.CommonResult": { + "type": "object", + "properties": { + "exception": { + "$ref": "#/definitions/exception.Builder" + }, + "httpCode": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..77f74d9 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,646 @@ +definitions: + data.User: + properties: + allow_public: + type: boolean + avatar: + type: string + bio: + type: string + email: + type: string + id: + type: integer + nickname: + type: string + permission_level: + type: integer + subtitle: + type: string + user_id: + type: string + username: + type: string + uuid: + type: string + type: object + data.UserSearchDoc: + properties: + avatar: + type: string + email: + type: string + nickname: + type: string + subtitle: + type: string + type: + type: string + user_id: + type: string + username: + type: string + type: object + exception.Builder: + properties: + endpoint: + type: string + error: {} + errorCode: + type: string + original: + type: string + service: + type: string + status: + type: string + type: + type: string + type: object + service_auth.ExchangeData: + properties: + client_id: + type: string + redirect_uri: + type: string + state: + type: string + type: object + service_auth.ExchangeResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + redirect_uri: + type: string + type: object + type: object + service_auth.MagicData: + properties: + client_id: + type: string + client_ip: + type: string + email: + type: string + redirect_uri: + type: string + state: + type: string + turnstile_token: + type: string + type: object + service_auth.MagicResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: {} + type: object + service_auth.RefreshData: + properties: + refresh_token: + type: string + type: object + service_auth.RefreshResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + $ref: '#/definitions/service_auth.TokenResponse' + type: object + service_auth.TokenData: + properties: + code: + type: string + type: object + service_auth.TokenResponse: + properties: + access_token: + type: string + refresh_token: + type: string + type: object + service_auth.TokenResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + $ref: '#/definitions/service_auth.TokenResponse' + type: object + service_event.CheckinQueryResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + checkin_at: + type: string + type: object + type: object + service_event.CheckinResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + checkin_code: + type: string + type: object + type: object + service_event.CheckinSubmitData: + properties: + checkin_code: + type: string + type: object + service_event.CheckinSubmitResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + type: object + service_event.InfoResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + properties: + end_time: + type: string + name: + type: string + start_time: + type: string + type: object + type: object + service_user.UserInfoData: + properties: + allow_public: + type: boolean + avatar: + type: string + bio: + type: string + email: + type: string + nickname: + type: string + permission_level: + type: integer + subtitle: + type: string + user_id: + type: string + username: + type: string + type: object + service_user.UserInfoResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + data: + $ref: '#/definitions/service_user.UserInfoData' + type: object + service_user.UserListResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + user_list: + items: + $ref: '#/definitions/data.UserSearchDoc' + type: array + type: object + service_user.UserTableResult: + properties: + common: + $ref: '#/definitions/shared.CommonResult' + user_table: + items: + $ref: '#/definitions/data.User' + type: array + type: object + shared.CommonResult: + properties: + exception: + $ref: '#/definitions/exception.Builder' + httpCode: + type: integer + type: object +info: + contact: {} +paths: + /auth/exchange: + post: + consumes: + - application/json + description: Exchanges client credentials and user session for a specific redirect + authorization code. + parameters: + - description: Exchange Request Credentials + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.ExchangeData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.ExchangeResult' + "400": + description: Invalid Input + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Exchange Auth Code + tags: + - Authentication + /auth/magic: + post: + consumes: + - application/json + description: Verifies Turnstile token and sends an authentication link via email. + Returns the URI directly if debug mode is enabled. + parameters: + - description: Magic Link Request Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.MagicData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.MagicResult' + "400": + description: Invalid Input + schema: + type: string + "403": + description: Turnstile Verification Failed + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Request Magic Link + tags: + - Authentication + /auth/redirect: + get: + consumes: + - application/x-www-form-urlencoded + description: Verifies the temporary email code, ensures the user exists (or + creates one), validates the client's redirect URI, and finally performs a + 302 redirect with a new authorization code. + parameters: + - description: Client Identifier + in: query + name: client_id + required: true + type: string + - description: Target Redirect URI + in: query + name: redirect_uri + required: true + type: string + - description: Temporary Verification Code + in: query + name: code + required: true + type: string + - description: Opaque state used to maintain state between the request and callback + in: query + name: state + type: string + produces: + - text/html + responses: + "302": + description: Redirect to the provided RedirectUri with a new code + schema: + type: string + "400": + description: Invalid Input / Client Not Found / URI Mismatch + schema: + type: string + "403": + description: Invalid or Expired Verification Code + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Handle Auth Callback and Redirect + tags: + - Authentication + /auth/refresh: + post: + consumes: + - application/json + description: Accepts a valid refresh token to issue a new access token and a + rotated refresh token. + parameters: + - description: Refresh Token Body + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.RefreshData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.RefreshResult' + "400": + description: Invalid Input + schema: + type: string + "401": + description: Invalid Refresh Token + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Refresh Access Token + tags: + - Authentication + /auth/token: + post: + consumes: + - application/json + description: Verifies the provided authorization code and issues a pair of JWT + tokens (Access and Refresh). + parameters: + - description: Token Request Body + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_auth.TokenData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_auth.TokenResult' + "400": + description: Invalid Input + schema: + type: string + "403": + description: Invalid or Expired Code + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Exchange Code for Token + tags: + - Authentication + /event/checkin: + get: + consumes: + - application/json + description: Creates a temporary check-in code for the authenticated user and + event. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.CheckinResult' + summary: Generate Check-in Code + tags: + - Event + /event/checkin/query: + get: + consumes: + - application/json + description: Returns the timestamp of when the user checked in, or null if not + yet checked in. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.CheckinQueryResult' + summary: Query Check-in Status + tags: + - Event + /event/checkin/submit: + post: + consumes: + - application/json + description: Submits the generated code to mark the user as attended. + parameters: + - description: Checkin Code Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_event.CheckinSubmitData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.CheckinSubmitResult' + summary: Submit Check-in Code + tags: + - Event + /event/info: + get: + consumes: + - application/json + description: Fetches the name, start time, and end time of an event using its + UUID. + parameters: + - description: Event UUID + in: query + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_event.InfoResult' + "400": + description: Invalid Input + schema: + type: string + "404": + description: Event Not Found + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Get Event Information + tags: + - Event + /user/full: + get: + consumes: + - application/json + description: Fetches all user records without pagination. This is typically + used for administrative overview or data export. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserTableResult' + "500": + description: Internal Server Error (Database Error) + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Get Full User Table + tags: + - User + /user/info: + get: + consumes: + - application/json + description: Fetches the complete profile data for the user associated with + the provided session/token. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserInfoResult' + "403": + description: Missing User ID / Unauthorized + schema: + type: string + "404": + description: User Not Found + schema: + type: string + "500": + description: Internal Server Error (UUID Parse Failed) + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Get My User Information + tags: + - User + /user/list: + get: + consumes: + - application/json + description: Fetches a list of users with support for pagination via limit and + offset. + parameters: + - description: Maximum number of users to return (default 0) + in: query + name: limit + type: string + - description: Number of users to skip + in: query + name: offset + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserListResult' + "400": + description: Invalid Input (Format Error) + schema: + type: string + "500": + description: Internal Server Error (Search Engine or Missing Offset) + schema: + type: string + summary: List Users + tags: + - User + /user/update: + patch: + consumes: + - application/json + description: |- + Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64). + Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars). + parameters: + - description: Updated User Profile Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_user.UserInfoData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service_user.UserInfoResult' + "400": + description: Invalid Input (Validation Failed) + schema: + type: string + "403": + description: Missing User ID / Unauthorized + schema: + type: string + "500": + description: Internal Server Error (Database Error / UUID Parse Failed) + schema: + type: string + security: + - ApiKeyAuth: [] + summary: Update User Information + tags: + - User +swagger: "2.0" diff --git a/go.mod b/go.mod index c345d84..a14de4b 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.15.0 go.opentelemetry.io/otel/sdk/metric v1.39.0 go.opentelemetry.io/otel/trace v1.39.0 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.47.0 golang.org/x/text v0.33.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 @@ -43,13 +43,16 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/ClickHouse/ch-go v0.61.5 // indirect github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.2 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect @@ -63,11 +66,22 @@ require ( github.com/go-faster/errors v0.7.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.29.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/goccy/go-yaml v1.19.1 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/go-version v1.6.0 // indirect @@ -77,10 +91,12 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -89,7 +105,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.57.1 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/segmentio/asm v1.2.0 // indirect @@ -99,6 +115,9 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.6.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect @@ -108,15 +127,18 @@ require ( go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/tools v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.5.7 // indirect ) diff --git a/go.sum b/go.sum index 9901b74..56b821d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,14 @@ github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeE github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo= github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= @@ -63,8 +71,12 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -76,6 +88,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -105,6 +118,38 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -113,6 +158,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk= github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -122,6 +169,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -176,6 +225,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -196,6 +247,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -229,6 +286,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2 h1:KYWnHK9pwzOUo3sNJlNmzRwZ5mw7opugn8njtGThKNg= github.com/redis/go-redis/extra/rediscmd/v9 v9.17.2/go.mod h1:wsfMQVl/GFYD9Gx/tlxurlTtvHkZRAt8j1qi27eIlTk= github.com/redis/go-redis/extra/redisotel/v9 v9.17.2 h1:wthFPRW3Y50CknMrjjJoYwXUFR4U7hMVJCMeLzDI8s4= @@ -273,6 +332,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= @@ -355,6 +422,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -366,6 +435,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -377,6 +448,7 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -391,6 +463,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -413,6 +487,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -429,6 +504,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -454,6 +531,7 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -470,6 +548,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -514,7 +596,10 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= diff --git a/internal/ali_cnrid/kyc.go b/internal/ali_cnrid/kyc.go index 228ca16..56baa46 100644 --- a/internal/ali_cnrid/kyc.go +++ b/internal/ali_cnrid/kyc.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "nixcn-cms/internal/cryptography" + "nixcn-cms/internal/kyc" "unicode/utf8" alicloudauth20190307 "github.com/alibabacloud-go/cloudauth-20190307/v4/client" @@ -18,21 +19,21 @@ import ( "github.com/spf13/viper" ) -func DecodeB64Json(b64Json string) (*KycInfo, error) { +func DecodeB64Json(b64Json string) (*kyc.KycInfo, error) { rawJson, err := base64.StdEncoding.DecodeString(b64Json) if err != nil { return nil, errors.New("[KYC] invalid base64 json") } - var kyc KycInfo - if err := json.Unmarshal(rawJson, &kyc); err != nil { + var kycInfo kyc.KycInfo + if err := json.Unmarshal(rawJson, &kycInfo); err != nil { return nil, errors.New("[KYC] invalid json structure") } - return &kyc, nil + return &kycInfo, nil } -func EncodeAES(kyc *KycInfo) (*string, error) { +func EncodeAES(kyc *kyc.KycInfo) (*string, error) { plainJson, err := json.Marshal(kyc) if err != nil { return nil, err @@ -47,22 +48,22 @@ func EncodeAES(kyc *KycInfo) (*string, error) { return &encrypted, nil } -func DecodeAES(cipherStr string) (*KycInfo, error) { +func DecodeAES(cipherStr string) (*kyc.KycInfo, error) { aesKey := viper.GetString("secrets.kyc_info_key") plainBytes, err := cryptography.AESCBCDecrypt(cipherStr, []byte(aesKey)) if err != nil { return nil, err } - var kyc KycInfo - if err := json.Unmarshal(plainBytes, &kyc); err != nil { + var kycInfo kyc.KycInfo + if err := json.Unmarshal(plainBytes, &kycInfo); err != nil { return nil, errors.New("[KYC] invalid decrypted json") } - return &kyc, nil + return &kycInfo, nil } -func MD5AliEnc(kyc *KycInfo) (*KycAli, error) { +func MD5AliEnc(kyc *kyc.KycInfo) (*KycAli, error) { if kyc.Type != "Chinese" { return nil, nil } diff --git a/server/server.go b/server/server.go index 9bbff23..52a547a 100644 --- a/server/server.go +++ b/server/server.go @@ -11,9 +11,23 @@ import ( "github.com/gin-gonic/gin" "github.com/spf13/viper" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" ) +// @title NixCN CMS API +// @version 1.0 +// @description API Docs based on Gin framework +// @termsOfService http://swagger.io/terms/ +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:8080 +// @BasePath /api/v1 +// @schemes http https func Start(ctx context.Context) { if !viper.GetBool("server.debug_mode") { gin.SetMode(gin.ReleaseMode) @@ -25,6 +39,8 @@ func Start(ctx context.Context) { r.Use(middleware.GinLogger()) r.Use(gin.Recovery()) + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + api.Handler(r.Group("/api/v1")) // Start http server diff --git a/service/service_auth/redirect.go b/service/service_auth/redirect.go index 4b612fb..7174386 100644 --- a/service/service_auth/redirect.go +++ b/service/service_auth/redirect.go @@ -2,7 +2,14 @@ package service_auth import ( "context" + "net/url" + "nixcn-cms/data" + "nixcn-cms/internal/authcode" + "nixcn-cms/internal/exception" "nixcn-cms/service/shared" + + "github.com/google/uuid" + "gorm.io/gorm" ) type RedirectData struct { @@ -23,5 +30,181 @@ type RedirectResult struct { } func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *RedirectResult) { + var err error + authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code) + if !ok { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectTokenInvalid). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 403, + Exception: exception, + }, + } + + return + } + + userData, err := new(data.User). + GetByEmail(payload.Context, &authCode.Email) + if err != nil { + if err == gorm.ErrRecordNotFound { + userData.UUID = uuid.New() + userData.UserId = uuid.New() + userData.Email = authCode.Email + userData.Username = userData.UserId.String() + userData.PermissionLevel = 10 + if err := userData.Create(payload.Context); err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + } else { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + } + + clientData := new(data.Client) + client, err := clientData.GetClientByClientId(payload.Context, payload.Data.ClientId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectClientNotFound). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + if err = client.ValidateRedirectURI(payload.Data.RedirectUri); err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectUriMismatch). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + newCode, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, authCode.Email) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + targetUrl, err := url.Parse(payload.Data.RedirectUri) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRedirectInvalidUri). + SetError(err). + Throw(payload.Context) + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + query := targetUrl.Query() + query.Set("code", newCode) + if payload.Data.State != "" { + query.Set("state", payload.Data.State) + } + targetUrl.RawQuery = query.Encode() + + result = &RedirectResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRedirect). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: targetUrl.String(), + } + + return } diff --git a/service/service_auth/refresh.go b/service/service_auth/refresh.go new file mode 100644 index 0000000..f04d847 --- /dev/null +++ b/service/service_auth/refresh.go @@ -0,0 +1,99 @@ +package service_auth + +import ( + "context" + "nixcn-cms/internal/authtoken" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + + "github.com/spf13/viper" +) + +type RefreshData struct { + RefreshToken string `json:"refresh_token"` +} + +type RefreshPayload struct { + Context context.Context + Data *RefreshData +} + +type RefreshResult struct { + Common shared.CommonResult + Data *TokenResponse +} + +func (self *AuthServiceImpl) Refresh(payload *RefreshPayload) (result *RefreshResult) { + JwtTool := authtoken.Token{ + Application: viper.GetString("server.application"), + } + + // 1. Refresh Access Token + accessToken, err := JwtTool.RefreshAccessToken(payload.Context, payload.Data.RefreshToken) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRefreshInvalidToken). + SetError(err). + Throw(payload.Context) + + result = &RefreshResult{ + Common: shared.CommonResult{ + HttpCode: 401, + Exception: exception, + }, + Data: nil, + } + + return + } + + // 2. Renew Refresh Token (Rotation) + refreshToken, err := JwtTool.RenewRefreshToken(payload.Context, payload.Data.RefreshToken) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthRefreshRenewFailed). + SetError(err). + Throw(payload.Context) + + result = &RefreshResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + // 3. Success Assignment + exception := new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceRefresh). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &RefreshResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &TokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, + } + + return +} diff --git a/service/service_auth/service.go b/service/service_auth/service.go index 423aa86..8bc5beb 100644 --- a/service/service_auth/service.go +++ b/service/service_auth/service.go @@ -3,6 +3,9 @@ package service_auth type AuthService interface { Exchange(*ExchangePayload) *ExchangeResult Magic(*MagicPayload) *MagicResult + Redirect(*RedirectPayload) *RedirectResult + Token(*TokenPayload) *TokenResult + Refresh(*RefreshPayload) *RefreshResult } type AuthServiceImpl struct{} diff --git a/service/service_auth/token.go b/service/service_auth/token.go new file mode 100644 index 0000000..0e5fd14 --- /dev/null +++ b/service/service_auth/token.go @@ -0,0 +1,118 @@ +package service_auth + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/authcode" + "nixcn-cms/internal/authtoken" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + + "github.com/spf13/viper" +) + +type TokenData struct { + Code string `json:"code"` +} + +type TokenPayload struct { + Context context.Context + Data *TokenData +} + +type TokenResult struct { + Common shared.CommonResult + Data *TokenResponse +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func (self *AuthServiceImpl) Token(payload *TokenPayload) (result *TokenResult) { + authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code) + if !ok { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthTokenInvalidToken). + Throw(payload.Context) + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 403, + Exception: exception, + }, + } + + return + } + + userData := new(data.User) + user, err := userData.GetByEmail(payload.Context, &authCode.Email) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + JwtTool := authtoken.Token{ + Application: viper.GetString("server.application"), + } + accessToken, refreshToken, err := JwtTool.IssueTokens(payload.Context, authCode.ClientId, user.UserId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeSpecific). + SetOriginal(exception.AuthTokenGenFailed). + SetError(err). + Throw(payload.Context) + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + result = &TokenResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceAuth). + SetEndpoint(exception.EndpointAuthServiceToken). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &TokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + }, + } + + return +} diff --git a/service/service_event/checkin.go b/service/service_event/checkin.go new file mode 100644 index 0000000..7079554 --- /dev/null +++ b/service/service_event/checkin.go @@ -0,0 +1,191 @@ +package service_event + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + "time" + + "github.com/google/uuid" +) + +type CheckinData struct { + EventId uuid.UUID `json:"event_id"` +} + +type CheckinPayload struct { + Context context.Context + UserId uuid.UUID + Data *CheckinData +} + +type CheckinResult struct { + Common shared.CommonResult + Data *struct { + CheckinCode *string `json:"checkin_code"` + } +} + +func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) { + attendance := &data.Attendance{UserId: payload.UserId} + code, err := attendance.GenCheckinCode(payload.Context, payload.Data.EventId) + if err != nil { + result = &CheckinResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventCheckinGenCodeFailed). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + result = &CheckinResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &struct { + CheckinCode *string `json:"checkin_code"` + }{code}, + } + return +} + +type CheckinSubmitData struct { + CheckinCode string `json:"checkin_code"` +} + +type CheckinSubmitPayload struct { + Context context.Context + Data *CheckinSubmitData +} + +type CheckinSubmitResult struct { + Common shared.CommonResult +} + +func (self *EventServiceImpl) CheckinSubmit(payload *CheckinSubmitPayload) (result *CheckinSubmitResult) { + attendanceData := new(data.Attendance) + err := attendanceData.VerifyCheckinCode(payload.Context, payload.Data.CheckinCode) + if err != nil { + result = &CheckinSubmitResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinSubmit). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + result = &CheckinSubmitResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinSubmit). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + } + return +} + +type CheckinQueryData struct { + EventId uuid.UUID `json:"event_id"` +} + +type CheckinQueryPayload struct { + Context context.Context + UserId uuid.UUID + Data *CheckinQueryData +} + +type CheckinQueryResult struct { + Common shared.CommonResult + Data *struct { + CheckinAt *time.Time `json:"checkin_at"` + } +} + +func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result *CheckinQueryResult) { + attendanceData := new(data.Attendance) + attendance, err := attendanceData.GetAttendance(payload.Context, payload.UserId, payload.Data.EventId) + + if err != nil { + result = &CheckinQueryResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + if attendance == nil { + result = &CheckinQueryResult{ + Common: shared.CommonResult{ + HttpCode: 404, + Exception: new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventCheckinQueryRecordNotFound). + Throw(payload.Context), + }, + } + return + } + + var checkinAt *time.Time + if !attendance.CheckinAt.IsZero() { + checkinAt = &attendance.CheckinAt + } + + result = &CheckinQueryResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckinQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &struct { + CheckinAt *time.Time `json:"checkin_at"` + }{checkinAt}, + } + return +} diff --git a/service/service_event/info.go b/service/service_event/info.go new file mode 100644 index 0000000..743fb21 --- /dev/null +++ b/service/service_event/info.go @@ -0,0 +1,78 @@ +package service_event + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/service/shared" + "time" + + "github.com/google/uuid" +) + +type InfoData struct { + EventId uuid.UUID `json:"event_id"` +} + +type InfoPayload struct { + Context context.Context + Data *InfoData +} + +type InfoResult struct { + Common shared.CommonResult + Data *struct { + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + } +} + +func (self *EventServiceImpl) Info(payload *InfoPayload) (result *InfoResult) { + event, err := new(data.Event).GetEventById(payload.Context, payload.Data.EventId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceInfo). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventInfoNotFound). + SetError(err). + Throw(payload.Context) + + result = &InfoResult{ + Common: shared.CommonResult{ + HttpCode: 404, + Exception: exception, + }, + } + + return + } + + resultData := struct { + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + }{ + Name: event.Name, + StartTime: event.StartTime, + EndTime: event.EndTime, + } + + result = &InfoResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: new(exception.Builder). + SetStatus(exception.StatusSuccess). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceInfo). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + Throw(payload.Context), + }, + Data: &resultData, + } + + return +} diff --git a/service/service_event/service.go b/service/service_event/service.go new file mode 100644 index 0000000..74bcf5a --- /dev/null +++ b/service/service_event/service.go @@ -0,0 +1,14 @@ +package service_event + +type EventService interface { + Checkin(*CheckinPayload) *CheckinResult + CheckinSubmit(*CheckinSubmitPayload) *CheckinSubmitResult + CheckinQuery(*CheckinQueryPayload) *CheckinQueryResult + Info(*InfoPayload) *InfoResult +} + +type EventServiceImpl struct{} + +func NewEventService() EventService { + return &EventServiceImpl{} +}