From 0ac96ab3e615c4710194139ba41f07764a5f78eb Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Sun, 1 Feb 2026 13:15:17 +0800 Subject: [PATCH] Add service_kyc Signed-off-by: Asai Neko --- api/kyc/handler.go | 14 +- api/kyc/query.go | 66 ++++ api/kyc/session.go | 68 ++++ cmd/gen_exception/definitions/common.yaml | 2 + cmd/gen_exception/definitions/endpoint.yaml | 4 + cmd/gen_exception/definitions/service.yaml | 1 + cmd/gen_exception/definitions/specific.yaml | 5 + data/attendance.go | 29 +- data/kyc.go | 92 +++++ data/user.go | 1 + docs/docs.go | 258 +++++++++++++ docs/swagger.json | 258 +++++++++++++ docs/swagger.yaml | 146 +++++++ internal/kyc/cnrid.go | 20 +- internal/kyc/crypto.go | 6 +- internal/kyc/passport.go | 107 +++--- internal/kyc/types.go | 14 +- service/service_event/checkin.go | 39 ++ service/service_event/join_event.go | 120 +++++- service/service_event/list_events.go | 4 +- service/service_kyc/query_kyc.go | 251 ++++++++++++ service/service_kyc/service.go | 15 + service/service_kyc/session_kyc.go | 400 ++++++++++++++++++++ 23 files changed, 1831 insertions(+), 89 deletions(-) create mode 100644 api/kyc/query.go create mode 100644 api/kyc/session.go create mode 100644 data/kyc.go create mode 100644 service/service_kyc/query_kyc.go create mode 100644 service/service_kyc/service.go create mode 100644 service/service_kyc/session_kyc.go diff --git a/api/kyc/handler.go b/api/kyc/handler.go index 452c43d..cfe13a4 100644 --- a/api/kyc/handler.go +++ b/api/kyc/handler.go @@ -2,10 +2,20 @@ package kyc import ( "nixcn-cms/middleware" + "nixcn-cms/service/service_kyc" "github.com/gin-gonic/gin" ) -func ApiHandler(r *gin.RouterGroup) { - r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10)) +type KycHandler struct { + svc service_kyc.KycService +} + +func ApiHandler(r *gin.RouterGroup) { + kycSvc := service_kyc.NewKycService() + kycHandler := &KycHandler{kycSvc} + + r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10)) + r.POST("/kyc/session", kycHandler.Session) + r.POST("/kyc/query", kycHandler.Query) } diff --git a/api/kyc/query.go b/api/kyc/query.go new file mode 100644 index 0000000..5a8ee9f --- /dev/null +++ b/api/kyc/query.go @@ -0,0 +1,66 @@ +package kyc + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_kyc" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// @Summary Query KYC Status +// @Description Checks the current state of a KYC session and updates local database if approved. +// @Tags KYC +// @Accept json +// @Produce json +// @Param payload body service_kyc.KycQueryData true "KYC query data (KycId)" +// @Success 200 {object} utils.RespStatus{data=service_kyc.KycQueryResponse} "Query processed (success/pending/failed)" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid UUID or input" +// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error" +// @Security ApiKeyAuth +// @Router /kyc/query [post] +func (self *KycHandler) Query(c *gin.Context) { + _, ok := c.Get("user_id") + if !ok { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorMissingUserId). + Throw(c). + String() + utils.HttpResponse(c, 403, errorCode) + return + } + + var queryData service_kyc.KycQueryData + if err := c.ShouldBindJSON(&queryData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + utils.HttpResponse(c, 400, errorCode) + return + } + + queryPayload := &service_kyc.KycQueryPayload{ + Context: c, + Data: &queryData, + } + + result := self.svc.QueryKyc(queryPayload) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data) +} diff --git a/api/kyc/session.go b/api/kyc/session.go new file mode 100644 index 0000000..8ac0e7f --- /dev/null +++ b/api/kyc/session.go @@ -0,0 +1,68 @@ +package kyc + +import ( + "nixcn-cms/internal/exception" + "nixcn-cms/service/service_kyc" + "nixcn-cms/utils" + + "github.com/gin-gonic/gin" +) + +// @Summary Create KYC Session +// @Description Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI. +// @Tags KYC +// @Accept json +// @Produce json +// @Param payload body service_kyc.KycSessionData true "KYC session data (Type and Base64 Identity)" +// @Success 200 {object} utils.RespStatus{data=service_kyc.KycSessionResponse} "Session created successfully" +// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid input or decode failed" +// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID" +// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / KYC Service Error" +// @Security ApiKeyAuth +// @Router /kyc/session [post] +func (self *KycHandler) Session(c *gin.Context) { + userIdFromHeaderOrig, ok := c.Get("user_id") + if !ok { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorMissingUserId). + Throw(c). + String() + utils.HttpResponse(c, 403, errorCode) + return + } + + var sessionData service_kyc.KycSessionData + if err := c.ShouldBindJSON(&sessionData); err != nil { + errorCode := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(c). + String() + utils.HttpResponse(c, 400, errorCode) + return + } + + sessionData.UserId = userIdFromHeaderOrig.(string) + + kycPayload := &service_kyc.KycSessionPayload{ + Context: c, + Data: &sessionData, + } + + result := self.svc.SessionKyc(kycPayload) + + if result.Common.Exception.Original != exception.CommonSuccess { + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String()) + return + } + + utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data) +} diff --git a/cmd/gen_exception/definitions/common.yaml b/cmd/gen_exception/definitions/common.yaml index baeec93..47fe57d 100644 --- a/cmd/gen_exception/definitions/common.yaml +++ b/cmd/gen_exception/definitions/common.yaml @@ -10,3 +10,5 @@ common: missing_user_id: "00007" user_not_found: "00008" user_not_public: "00009" + base64_decode_failed: "00010" + json_decode_failed: "00011" diff --git a/cmd/gen_exception/definitions/endpoint.yaml b/cmd/gen_exception/definitions/endpoint.yaml index 17f00b9..d8c6a71 100644 --- a/cmd/gen_exception/definitions/endpoint.yaml +++ b/cmd/gen_exception/definitions/endpoint.yaml @@ -21,5 +21,9 @@ endpoint: list: "03" full: "04" create: "05" + kyc: + service: + session: "01" + query: "02" middleware: service: "01" diff --git a/cmd/gen_exception/definitions/service.yaml b/cmd/gen_exception/definitions/service.yaml index eb6ee05..be3669e 100644 --- a/cmd/gen_exception/definitions/service.yaml +++ b/cmd/gen_exception/definitions/service.yaml @@ -2,3 +2,4 @@ service: auth: "001" user: "002" event: "003" + kyc: "004" diff --git a/cmd/gen_exception/definitions/specific.yaml b/cmd/gen_exception/definitions/specific.yaml index 6ca6034..1d97cc1 100644 --- a/cmd/gen_exception/definitions/specific.yaml +++ b/cmd/gen_exception/definitions/specific.yaml @@ -36,3 +36,8 @@ event: database_failed: "00001" join: event_invalid: "00001" +kyc: + session: + failed: "00001" + query: + failed: "00001" diff --git a/data/attendance.go b/data/attendance.go index a42ced1..b1ca2ca 100644 --- a/data/attendance.go +++ b/data/attendance.go @@ -19,9 +19,9 @@ type Attendance struct { AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;uniqueIndex;not null"` EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` + KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` Role string `json:"role" gorm:"type:varchar(255);not null"` State string `json:"state" gorm:"type:varchar(255);not null"` // suspended | out_of_limit | success - KycInfo string `json:"kyc_info" gorm:"type:text"` CheckinAt time.Time `json:"checkin_at"` } @@ -43,6 +43,11 @@ func (self *Attendance) SetUserId(s uuid.UUID) *Attendance { return self } +func (self *Attendance) SetKycId(s uuid.UUID) *Attendance { + self.KycId = s + return self +} + func (self *Attendance) SetRole(s string) *Attendance { self.Role = s return self @@ -53,11 +58,6 @@ func (self *Attendance) SetState(s string) *Attendance { return self } -func (self *Attendance) SetKycInfo(s string) *Attendance { - self.KycInfo = s - return self -} - func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) { var checkin Attendance @@ -112,6 +112,23 @@ func (self *Attendance) GetEventsByUserID(ctx context.Context, userID uuid.UUID) return &result, err } +func (self *Attendance) GetAttendanceByEventIdAndUserId(ctx context.Context, eventId, userId uuid.UUID) (*Attendance, error) { + var attendance Attendance + + err := Database.WithContext(ctx). + Where("event_id = ? AND user_id = ?", eventId, userId). + First(&attendance).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + + return &attendance, nil +} + func (self *Attendance) Create(ctx context.Context) error { self.UUID = uuid.New() self.AttendanceId = uuid.New() diff --git a/data/kyc.go b/data/kyc.go new file mode 100644 index 0000000..9c94bd3 --- /dev/null +++ b/data/kyc.go @@ -0,0 +1,92 @@ +package data + +import ( + "context" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Kyc struct { + Id uint `json:"id" gorm:"primarykey;autoincrement"` + UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"` + UserId uuid.UUID `json:"user_id" gorm:"type:uuid;not null"` // 已移除 uniqueindex + KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueindex;not null"` + Type string `json:"type" gorm:"type:varchar(255);not null"` + KycInfo string `json:"kyc_info" gorm:"type:text"` // aes256(base64) +} + +func (self *Kyc) SetUserId(id uuid.UUID) *Kyc { + self.UserId = id + return self +} + +func (self *Kyc) SetType(t string) *Kyc { + self.Type = t + return self +} + +func (self *Kyc) SetKycInfo(info string) *Kyc { + self.KycInfo = info + return self +} + +func (self *Kyc) Create(ctx context.Context) (uuid.UUID, error) { + self.UUID = uuid.New() + self.KycId = uuid.New() + + err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + return tx.Create(self).Error + }) + + if err != nil { + return uuid.Nil, err + } + + return self.KycId, nil +} + +func (self *Kyc) GetByKycId(ctx context.Context, kycId *uuid.UUID) (*Kyc, error) { + var kyc Kyc + err := Database.WithContext(ctx). + Where("kyc_id = ?", kycId). + First(&kyc).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &kyc, nil +} + +func (self *Kyc) ListByUserId(ctx context.Context, userId *uuid.UUID) ([]Kyc, error) { + var list []Kyc + err := Database.WithContext(ctx). + Where("user_id = ?", userId). + Find(&list).Error + + return list, err +} + +func (self *Kyc) UpdateByKycID(ctx context.Context, kycId *uuid.UUID, updates map[string]any) error { + return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + return tx.Model(&Kyc{}). + Where("kyc_id = ?", kycId). + Updates(updates).Error + }) +} + +func (self *Kyc) DeleteByKycID(ctx context.Context, kycId *uuid.UUID) error { + return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + return tx.Where("kyc_id = ?", kycId). + Delete(&Kyc{}).Error + }) +} + +func (self *Kyc) DeleteAllByUserId(ctx context.Context, userId *uuid.UUID) error { + return Database.WithContext(ctx). + Where("user_id = ?", userId). + Delete(&Kyc{}).Error +} diff --git a/data/user.go b/data/user.go index 80a6d1d..957fa2a 100644 --- a/data/user.go +++ b/data/user.go @@ -19,6 +19,7 @@ type User struct { Bio string `json:"bio" gorm:"type:text"` PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"` AllowPublic bool `json:"allow_public" gorm:"default:false;not null"` + KycInfo string `json:"kyc_info" gorm:"type:text"` } type UserIndexDoc struct { diff --git a/docs/docs.go b/docs/docs.go index 1b8a828..48de69b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1144,6 +1144,216 @@ const docTemplate = `{ } } }, + "/kyc/query": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Checks the current state of a KYC session and updates local database if approved.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "KYC" + ], + "summary": "Query KYC Status", + "parameters": [ + { + "description": "KYC query data (KycId)", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_kyc.KycQueryData" + } + } + ], + "responses": { + "200": { + "description": "Query processed (success/pending/failed)", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_kyc.KycQueryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid UUID or input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "403": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + } + } + } + }, + "/kyc/session": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "KYC" + ], + "summary": "Create KYC Session", + "parameters": [ + { + "description": "KYC session data (Type and Base64 Identity)", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_kyc.KycSessionData" + } + } + ], + "responses": { + "200": { + "description": "Session created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_kyc.KycSessionResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid input or decode failed", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "403": { + "description": "Missing User ID", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error / KYC Service Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + } + } + } + }, "/user/info": { "get": { "security": [ @@ -1739,6 +1949,54 @@ const docTemplate = `{ "properties": { "event_id": { "type": "string" + }, + "kyc_id": { + "type": "string" + } + } + }, + "service_kyc.KycQueryData": { + "type": "object", + "properties": { + "kyc_id": { + "type": "string" + } + } + }, + "service_kyc.KycQueryResponse": { + "type": "object", + "properties": { + "status": { + "description": "success | pending | failed", + "type": "string" + } + } + }, + "service_kyc.KycSessionData": { + "type": "object", + "properties": { + "identity": { + "description": "base64 json", + "type": "string" + }, + "type": { + "description": "cnrid | passport", + "type": "string" + } + } + }, + "service_kyc.KycSessionResponse": { + "type": "object", + "properties": { + "kyc_id": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "status": { + "description": "success | processing", + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index d02d5dc..13cf1cb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1142,6 +1142,216 @@ } } }, + "/kyc/query": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Checks the current state of a KYC session and updates local database if approved.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "KYC" + ], + "summary": "Query KYC Status", + "parameters": [ + { + "description": "KYC query data (KycId)", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_kyc.KycQueryData" + } + } + ], + "responses": { + "200": { + "description": "Query processed (success/pending/failed)", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_kyc.KycQueryResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid UUID or input", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "403": { + "description": "Unauthorized", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + } + } + } + }, + "/kyc/session": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "KYC" + ], + "summary": "Create KYC Session", + "parameters": [ + { + "description": "KYC session data (Type and Base64 Identity)", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service_kyc.KycSessionData" + } + } + ], + "responses": { + "200": { + "description": "Session created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service_kyc.KycSessionResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid input or decode failed", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "403": { + "description": "Missing User ID", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error / KYC Service Error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.RespStatus" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + } + } + } + }, "/user/info": { "get": { "security": [ @@ -1737,6 +1947,54 @@ "properties": { "event_id": { "type": "string" + }, + "kyc_id": { + "type": "string" + } + } + }, + "service_kyc.KycQueryData": { + "type": "object", + "properties": { + "kyc_id": { + "type": "string" + } + } + }, + "service_kyc.KycQueryResponse": { + "type": "object", + "properties": { + "status": { + "description": "success | pending | failed", + "type": "string" + } + } + }, + "service_kyc.KycSessionData": { + "type": "object", + "properties": { + "identity": { + "description": "base64 json", + "type": "string" + }, + "type": { + "description": "cnrid | passport", + "type": "string" + } + } + }, + "service_kyc.KycSessionResponse": { + "type": "object", + "properties": { + "kyc_id": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "status": { + "description": "success | processing", + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 47a025d..66aaf12 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -104,6 +104,38 @@ definitions: properties: event_id: type: string + kyc_id: + type: string + type: object + service_kyc.KycQueryData: + properties: + kyc_id: + type: string + type: object + service_kyc.KycQueryResponse: + properties: + status: + description: success | pending | failed + type: string + type: object + service_kyc.KycSessionData: + properties: + identity: + description: base64 json + type: string + type: + description: cnrid | passport + type: string + type: object + service_kyc.KycSessionResponse: + properties: + kyc_id: + type: string + redirect_uri: + type: string + status: + description: success | processing + type: string type: object service_user.UserInfoData: properties: @@ -769,6 +801,120 @@ paths: summary: List Events tags: - Event + /kyc/query: + post: + consumes: + - application/json + description: Checks the current state of a KYC session and updates local database + if approved. + parameters: + - description: KYC query data (KycId) + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_kyc.KycQueryData' + produces: + - application/json + responses: + "200": + description: Query processed (success/pending/failed) + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_kyc.KycQueryResponse' + type: object + "400": + description: Invalid UUID or input + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "403": + description: Unauthorized + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "500": + description: Internal Server Error + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + security: + - ApiKeyAuth: [] + summary: Query KYC Status + tags: + - KYC + /kyc/session: + post: + consumes: + - application/json + description: Initializes a KYC process (CNRid or Passport) and returns the status + or redirect URI. + parameters: + - description: KYC session data (Type and Base64 Identity) + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service_kyc.KycSessionData' + produces: + - application/json + responses: + "200": + description: Session created successfully + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + $ref: '#/definitions/service_kyc.KycSessionResponse' + type: object + "400": + description: Invalid input or decode failed + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "403": + description: Missing User ID + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + "500": + description: Internal Server Error / KYC Service Error + schema: + allOf: + - $ref: '#/definitions/utils.RespStatus' + - properties: + data: + type: object + type: object + security: + - ApiKeyAuth: [] + summary: Create KYC Session + tags: + - KYC /user/info: get: consumes: diff --git a/internal/kyc/cnrid.go b/internal/kyc/cnrid.go index 44dd836..cb8f0c9 100644 --- a/internal/kyc/cnrid.go +++ b/internal/kyc/cnrid.go @@ -14,17 +14,13 @@ import ( "github.com/spf13/viper" ) -func CNRidMD5AliEnc(kyc *KycInfo) (*AliCloudAuth, error) { - if kyc.Type != "Chinese" { - return nil, nil - } - +func CNRidMD5AliEnc(kyc *CNRidInfo) (*AliCloudAuth, error) { // MD5 Legal Name rule: First Chinese char md5enc, remaining plain, at least 2 Chinese chars - if len(kyc.CNRidInfo.LegalName) < 2 || utf8.RuneCountInString(kyc.CNRidInfo.LegalName) < 2 { + if len(kyc.LegalName) < 2 || utf8.RuneCountInString(kyc.LegalName) < 2 { return nil, fmt.Errorf("input string must have at least 2 Chinese characters") } - lnFirstRune, size := utf8.DecodeRuneInString(kyc.CNRidInfo.LegalName) + lnFirstRune, size := utf8.DecodeRuneInString(kyc.LegalName) if lnFirstRune == utf8.RuneError { return nil, fmt.Errorf("invalid first character") } @@ -33,18 +29,18 @@ func CNRidMD5AliEnc(kyc *KycInfo) (*AliCloudAuth, error) { lnHash.Write([]byte(string(lnFirstRune))) lnFirstHash := hex.EncodeToString(lnHash.Sum(nil)) - lnRemaining := kyc.CNRidInfo.LegalName[size:] + lnRemaining := kyc.LegalName[size:] ln := lnFirstHash + lnRemaining // MD5 Resident Id rule: First 6 char plain, middle birthdate md5enc, last 4 char plain, at least 18 chars - if len(kyc.CNRidInfo.ResidentId) < 18 { + if len(kyc.ResidentId) < 18 { return nil, fmt.Errorf("input string must have at least 18 characters") } - ridPrefix := kyc.CNRidInfo.ResidentId[:6] - ridSuffix := kyc.CNRidInfo.ResidentId[len(kyc.CNRidInfo.ResidentId)-4:] - ridMiddle := kyc.CNRidInfo.ResidentId[6 : len(kyc.CNRidInfo.ResidentId)-4] + ridPrefix := kyc.ResidentId[:6] + ridSuffix := kyc.ResidentId[len(kyc.ResidentId)-4:] + ridMiddle := kyc.ResidentId[6 : len(kyc.ResidentId)-4] ridHash := md5.New() ridHash.Write([]byte(ridMiddle)) diff --git a/internal/kyc/crypto.go b/internal/kyc/crypto.go index 3d3ae77..19cf0b1 100644 --- a/internal/kyc/crypto.go +++ b/internal/kyc/crypto.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/viper" ) -func EncodeAES(kyc *KycInfo) (*string, error) { +func EncodeAES(kyc any) (*string, error) { plainJson, err := json.Marshal(kyc) if err != nil { return nil, err @@ -23,14 +23,14 @@ func EncodeAES(kyc *KycInfo) (*string, error) { return &encrypted, nil } -func DecodeAES(cipherStr string) (*KycInfo, error) { +func DecodeAES(cipherStr string) (any, error) { aesKey := viper.GetString("secrets.kyc_info_key") plainBytes, err := cryptography.AESCBCDecrypt(cipherStr, []byte(aesKey)) if err != nil { return nil, err } - var kycInfo KycInfo + var kycInfo any if err := json.Unmarshal(plainBytes, &kycInfo); err != nil { return nil, errors.New("[KYC] invalid decrypted json") } diff --git a/internal/kyc/passport.go b/internal/kyc/passport.go index 2f14306..69ad23a 100644 --- a/internal/kyc/passport.go +++ b/internal/kyc/passport.go @@ -2,6 +2,7 @@ package kyc import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -11,78 +12,80 @@ import ( "github.com/spf13/viper" ) -func CreateSession() (*PassportReaderSessionResponse, error) { +const ( + StateCreated = "CREATED" + StateInitiated = "INITIATED" + StateFailed = "FAILED" + StateAborted = "ABORTED" + StateCompleted = "COMPLETED" + StateRejected = "REJECTED" + StateApproved = "APPROVED" +) + +func doPassportRequest(ctx context.Context, method, path string, body any, target any) error { publicKey := viper.GetString("kyc.passport_reader_public_key") secret := viper.GetString("kyc.passport_reader_secret") + baseURL := "https://passportreader.app/api/v1" - apiURL := "https://passportreader.app/api/v1/session.create" - - client := &http.Client{ - Timeout: 10 * time.Second, + var bodyReader io.Reader + if body != nil { + jsonData, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("marshal request failed: %w", err) + } + bodyReader = bytes.NewBuffer(jsonData) } - req, err := http.NewRequest("POST", apiURL, nil) + req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bodyReader) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return fmt.Errorf("create request failed: %w", err) } req.SetBasicAuth(publicKey, secret) - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + if body != nil { + req.Header.Set("Content-Type", "application/json") } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("api returned error: %d, body: %s", resp.StatusCode, string(body)) - } - - var sessionResp PassportReaderSessionResponse - if err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } - - return &sessionResp, nil -} - -func GetSessionDetails(sessionID int) (*PassportReaderSessionDetailResponse, error) { - publicKey := viper.GetString("kyc.passport_reader_public_key") - secret := viper.GetString("kyc.passport_reader_secret") - - apiURL := "https://passportreader.app/api/v1/session.get" - - reqPayload := PassportReaderGetSessionRequest{ID: sessionID} - jsonData, err := json.Marshal(reqPayload) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - - req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.SetBasicAuth(publicKey, secret) - req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return fmt.Errorf("http request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("api error: %d, response: %s", resp.StatusCode, string(body)) + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("api error: status %d, body %s", resp.StatusCode, string(respBody)) } - var detailResp PassportReaderSessionDetailResponse - if err := json.NewDecoder(resp.Body).Decode(&detailResp); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) + if target != nil { + if err := json.NewDecoder(resp.Body).Decode(target); err != nil { + return fmt.Errorf("decode response failed: %w", err) + } } - return &detailResp, nil + return nil +} + +func CreateSession(ctx context.Context) (*PassportReaderSessionResponse, error) { + var resp PassportReaderSessionResponse + err := doPassportRequest(ctx, "POST", "/session.create", nil, &resp) + return &resp, err +} + +func GetSessionState(ctx context.Context, sessionID int) (string, error) { + payload := PassportReaderGetSessionRequest{ID: sessionID} + var resp PassportReaderStateResponse + err := doPassportRequest(ctx, "POST", "/session.state", payload, &resp) + if err != nil { + return "", err + } + return resp.State, nil +} + +func GetSessionDetails(ctx context.Context, sessionID int) (*PassportReaderSessionDetailResponse, error) { + payload := PassportReaderGetSessionRequest{ID: sessionID} + var resp PassportReaderSessionDetailResponse + err := doPassportRequest(ctx, "POST", "/session.get", payload, &resp) + return &resp, err } diff --git a/internal/kyc/types.go b/internal/kyc/types.go index 84a865a..bcbd63f 100644 --- a/internal/kyc/types.go +++ b/internal/kyc/types.go @@ -1,17 +1,15 @@ package kyc -type KycInfo struct { - Type string `json:"type"` // cnrid/passport - CNRidInfo *CNRidInfo `json:"CNRodInfo"` - PassportInfo *PassportInfo `json:"passport_info"` -} - type CNRidInfo struct { LegalName string `json:"legal_name"` ResidentId string `json:"resident_id"` } type PassportInfo struct { + ID string `json:"id"` +} + +type PassportResp struct { GivenNames string `json:"given_names"` Surname string `json:"surname"` Nationality string `json:"nationality"` @@ -36,6 +34,10 @@ type PassportReaderGetSessionRequest struct { ID int `json:"id"` } +type PassportReaderStateResponse struct { + State string `json:"state"` +} + type PassportReaderSessionDetailResponse struct { State string `json:"state"` GivenNames string `json:"given_names"` diff --git a/service/service_event/checkin.go b/service/service_event/checkin.go index ba89bb1..fca2816 100644 --- a/service/service_event/checkin.go +++ b/service/service_event/checkin.go @@ -30,6 +30,45 @@ type CheckinResult struct { } func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) { + attendandeData, err := new(data.Attendance). + GetAttendanceByEventIdAndUserId(payload.Context, payload.Data.EventId, payload.UserId) + 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.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context), + }, + } + return + } + + eventData, err := new(data.Event). + GetEventById(payload.Context, payload.Data.EventId) + + if attendandeData.KycId == uuid.Nil && eventData.EnableKYC == true { + result = &CheckinResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceCheckin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context), + }, + } + return + } + attendance := &data.Attendance{UserId: payload.UserId} code, err := attendance.GenCheckinCode(payload.Context, payload.Data.EventId) if err != nil { diff --git a/service/service_event/join_event.go b/service/service_event/join_event.go index 84c92f3..1cca317 100644 --- a/service/service_event/join_event.go +++ b/service/service_event/join_event.go @@ -12,6 +12,7 @@ import ( type EventJoinData struct { EventId string `json:"event_id"` + KycId string `json:"kyc_id"` UserId string `json:"user_id" swaggerignore:"true"` Role string `json:"role" swaggerignore:"true"` State string `json:"state" swaggerignore:"true"` @@ -30,7 +31,6 @@ func (self *EventServiceImpl) JoinEvent(payload *EventJoinPayload) (result *Even var err error attendenceData := new(data.Attendance) - eventData := new(data.Event) eventId, err := uuid.Parse(payload.Data.EventId) if err != nil { @@ -53,19 +53,41 @@ func (self *EventServiceImpl) JoinEvent(payload *EventJoinPayload) (result *Even return } - if !eventData.EndTime.Before(time.Now()) { + eventData, err := new(data.Event).GetEventById(payload.Context, eventId) + + if err != nil { exception := new(exception.Builder). SetStatus(exception.StatusServer). SetService(exception.ServiceEvent). SetEndpoint(exception.EndpointEventServiceJoin). - SetType(exception.TypeSpecific). - SetOriginal(exception.EventJoinEventInvalid). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). SetError(err). Throw(payload.Context) result = &EventJoinResult{ Common: shared.CommonResult{ - HttpCode: 403, + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + if eventData.EnableKYC == true && payload.Data.KycId == "" { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(nil). + Throw(payload.Context) + + result = &EventJoinResult{ + Common: shared.CommonResult{ + HttpCode: 400, Exception: exception, }, } @@ -94,8 +116,94 @@ func (self *EventServiceImpl) JoinEvent(payload *EventJoinPayload) (result *Even return } - attendenceData.SetEventId(eventId) + if eventData.EnableKYC == true && payload.Data.KycId != "" { + kycId, err := uuid.Parse(payload.Data.KycId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(payload.Context) + + result = &EventJoinResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + kycData, err := new(data.Kyc).GetByKycId(payload.Context, &kycId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context) + + result = &EventJoinResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + } + + return + } + + if kycData.UserId != userId { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context) + + result = &EventJoinResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + } + + return + } + + attendenceData.SetKycId(kycData.KycId) + } + + if !eventData.EndTime.Before(time.Now()) { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeSpecific). + SetOriginal(exception.EventJoinEventInvalid). + SetError(err). + Throw(payload.Context) + + result = &EventJoinResult{ + Common: shared.CommonResult{ + HttpCode: 403, + Exception: exception, + }, + } + + return + } + attendenceData.SetUserId(userId) + attendenceData.SetEventId(eventId) attendenceData.SetRole("notmal") attendenceData.SetState("success") diff --git a/service/service_event/list_events.go b/service/service_event/list_events.go index ece5b08..c8221c2 100644 --- a/service/service_event/list_events.go +++ b/service/service_event/list_events.go @@ -29,7 +29,7 @@ func (self *EventServiceImpl) ListEvents(payload *EventListPayload) (result *Eve var offset string if payload.Offset == nil || *payload.Offset == "" { - exc := new(exception.Builder). + exception := new(exception.Builder). SetStatus(exception.StatusUser). SetService(exception.ServiceEvent). SetEndpoint(exception.EndpointEventServiceList). @@ -41,7 +41,7 @@ func (self *EventServiceImpl) ListEvents(payload *EventListPayload) (result *Eve return &EventListResult{ Common: shared.CommonResult{ HttpCode: 400, - Exception: exc, + Exception: exception, }, Data: nil, } diff --git a/service/service_kyc/query_kyc.go b/service/service_kyc/query_kyc.go new file mode 100644 index 0000000..d662455 --- /dev/null +++ b/service/service_kyc/query_kyc.go @@ -0,0 +1,251 @@ +package service_kyc + +import ( + "context" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/internal/kyc" + "nixcn-cms/service/shared" + + "github.com/google/uuid" +) + +type KycQueryData struct { + KycId string `json:"kyc_id"` +} + +type KycQueryPayload struct { + Context context.Context + Data *KycQueryData +} + +type KycQueryResponse struct { + Status string `json:"status"` // success | pending | failed +} + +type KycQueryResult struct { + Common shared.CommonResult + Data *KycQueryResponse +} + +func (self *KycServiceImpl) QueryKyc(payload *KycQueryPayload) (result *KycQueryResult) { + var err error + + sessionState, err := kyc.GetSessionState(payload.Context, self.PassportReaderSessionId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + if sessionState == kyc.StateApproved { + sessionDetails, err := kyc.GetSessionDetails(payload.Context, self.PassportReaderSessionId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + var kycInfo = kyc.PassportResp{ + GivenNames: sessionDetails.GivenNames, + Surname: sessionDetails.Surname, + Nationality: sessionDetails.Nationality, + DateOfBirth: sessionDetails.DateOfBirth, + DocumentType: sessionDetails.DocumentType, + DocumentNumber: sessionDetails.DocumentNumber, + ExpiryDate: sessionDetails.ExpiryDate, + } + + if kycInfo.DocumentType != "PASSPORT" || kycInfo.DocumentNumber != self.PassportId { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + encodedKycInfo, err := kyc.EncodeAES(kycInfo) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeSpecific). + SetOriginal(exception.KycQueryFailed). + SetError(err). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycId, err := uuid.Parse(payload.Data.KycId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + var updates = map[string]any{ + "kyc_info": kycInfo, + } + + err = new(data.Kyc). + SetKycInfo(*encodedKycInfo). + UpdateByKycID(payload.Context, &kycId, updates) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &KycQueryResponse{ + Status: "success", + }, + } + + return + } + + if sessionState == kyc.StateCreated || sessionState == kyc.StateInitiated || sessionState == kyc.StateCompleted { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &KycQueryResponse{ + Status: "pending", + }, + } + + return + } + + if sessionState == kyc.StateFailed || sessionState == kyc.StateAborted || sessionState == kyc.StateRejected { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceQuery). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &KycQueryResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &KycQueryResponse{ + Status: "failed", + }, + } + + return + } + + return +} diff --git a/service/service_kyc/service.go b/service/service_kyc/service.go new file mode 100644 index 0000000..4770111 --- /dev/null +++ b/service/service_kyc/service.go @@ -0,0 +1,15 @@ +package service_kyc + +type KycService interface { + SessionKyc(*KycSessionPayload) *KycSessionResult + QueryKyc(*KycQueryPayload) *KycQueryResult +} + +type KycServiceImpl struct { + PassportId string `json:"passport_id"` + PassportReaderSessionId int `json:"passport_reader_session_id"` +} + +func NewKycService() KycService { + return &KycServiceImpl{} +} diff --git a/service/service_kyc/session_kyc.go b/service/service_kyc/session_kyc.go new file mode 100644 index 0000000..aaa3a16 --- /dev/null +++ b/service/service_kyc/session_kyc.go @@ -0,0 +1,400 @@ +package service_kyc + +import ( + "context" + "encoding/base64" + "encoding/json" + "net/url" + "nixcn-cms/data" + "nixcn-cms/internal/exception" + "nixcn-cms/internal/kyc" + "nixcn-cms/service/shared" + + "github.com/google/uuid" +) + +// cnrid: {"legal_name":"", "resident_id":""} +// passport: {"id": ""} +type KycSessionData struct { + Type string `json:"type"` // cnrid | passport + Identity string `json:"identity"` // base64 json + UserId string `json:"user_id" swaggerignore:"true"` +} + +type KycSessionPayload struct { + Context context.Context + Data *KycSessionData +} + +type KycSessionResponse struct { + Status string `json:"status"` // success | processing + KycId *string `json:"kyc_id"` + RedirectUri *string `json:"redirect_uri"` +} + +type KycSessionResult struct { + Common shared.CommonResult + Data *KycSessionResponse +} + +func (self *KycServiceImpl) SessionKyc(payload *KycSessionPayload) (result *KycSessionResult) { + var err error + + decodedIdentityByte, err := base64.StdEncoding.DecodeString(payload.Data.Identity) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorBase64DecodeFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + switch payload.Data.Type { + case "cnrid": + var info kyc.CNRidInfo + if err := json.Unmarshal(decodedIdentityByte, &info); err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorJsonDecodeFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycInfo := kyc.CNRidInfo{ + LegalName: info.LegalName, + ResidentId: info.ResidentId, + } + + aliCloudAuth, err := kyc.CNRidMD5AliEnc(&kycInfo) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycResult, err := kyc.AliId2MetaVerify(aliCloudAuth) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + if *kycResult != "1" { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeSpecific). + SetOriginal(exception.KycSessionFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + userId, err := uuid.Parse(payload.Data.UserId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + encodedKycInfo, err := kyc.EncodeAES(kycInfo) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeSpecific). + SetOriginal(exception.KycSessionFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycIdOrig, err := new(data.Kyc). + SetType("cnrid"). + SetUserId(userId). + SetKycInfo(*encodedKycInfo). + Create(payload.Context) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycId := kycIdOrig.String() + + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &KycSessionResponse{ + Status: "success", + KycId: &kycId, + RedirectUri: nil, + }, + } + + case "passport": + var info kyc.PassportInfo + if err := json.Unmarshal(decodedIdentityByte, &info); err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorJsonDecodeFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + self.PassportId = info.ID + + sessionResponse, err := kyc.CreateSession(payload.Context) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInternal). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + self.PassportReaderSessionId = sessionResponse.ID + + userId, err := uuid.Parse(payload.Data.UserId) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusServer). + SetService(exception.ServiceEvent). + SetEndpoint(exception.EndpointEventServiceJoin). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorUuidParseFailed). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycIdOrig, err := new(data.Kyc). + SetType("passport"). + SetUserId(userId). + Create(payload.Context) + if err != nil { + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorDatabase). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 500, + Exception: exception, + }, + Data: nil, + } + + return + } + + kycId := kycIdOrig.String() + kycBaseURL := "https://passportreader.app/open" + kycUrl, _ := url.Parse(kycBaseURL) + kycQuery := kycUrl.Query() + kycQuery.Set("token", sessionResponse.Token) + kycUrl.RawQuery = kycQuery.Encode() + + redirectUri := kycUrl.String() + + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonSuccess). + SetError(nil). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 200, + Exception: exception, + }, + Data: &KycSessionResponse{ + Status: "processing", + KycId: &kycId, + RedirectUri: &redirectUri, + }, + } + + default: + exception := new(exception.Builder). + SetStatus(exception.StatusUser). + SetService(exception.ServiceKyc). + SetEndpoint(exception.EndpointKycServiceSession). + SetType(exception.TypeCommon). + SetOriginal(exception.CommonErrorInvalidInput). + SetError(err). + Throw(payload.Context) + + result = &KycSessionResult{ + Common: shared.CommonResult{ + HttpCode: 400, + Exception: exception, + }, + Data: nil, + } + + return + } + + return +}