From 66c85dd65072fd9e47a0142b02c746cbf161a588 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 27 Aug 2024 15:40:00 +0800 Subject: [PATCH] add login --- Makefile | 15 +- etc/gateway.yaml | 1 + gateway.json | 200 ++++++++++++++++-- generate/api/member.api | 22 +- go.mod | 1 + internal/config/config.go | 1 + internal/domain/payload.go | 14 ++ internal/handler/member/info_handler.go | 2 +- internal/handler/member/logout_handler.go | 2 +- .../logic/member/check_verify_code_logic.go | 5 +- .../member/forget_password_code_logic.go | 46 ++-- internal/logic/member/info_logic.go | 35 ++- internal/logic/member/logout_logic.go | 19 +- .../logic/member/upadte_password_logic.go | 5 +- internal/middleware/auth_middleware.go | 82 ++++++- internal/payload/token.go | 22 ++ internal/svc/service_context.go | 10 +- internal/types/types.go | 14 ++ 18 files changed, 423 insertions(+), 73 deletions(-) create mode 100644 internal/domain/payload.go create mode 100644 internal/payload/token.go diff --git a/Makefile b/Makefile index c0de6aa..86a6493 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,19 @@ #goctl api plugin -plugin goctl-swagger="swagger -filename gateway.json -host dev-api.30cm.net" -api ./generate/api/gateway.api -dir . #goctl api go -api ./generate/api/gateway.api -dir . -style go_zero -GOFMT ?= gofmt "-s" +GOFMT ?= gofmt GOFILES := $(shell find . -name "*.go") .PHONY: fmt fmt: # 格式優化 - $(GOFMT) -w $(GOFILES) - goimports -w ./ \ No newline at end of file + $(GOFMT) -s -w $(GOFILES) + goimports -w ./ + + +.PHONY: gen-doc +gen-doc: # 格式優化 + goctl api plugin -plugin goctl-swagger="swagger -filename gateway.json -host dev-api.30cm.net" -api ./generate/api/gateway.api -dir . + +.PHONY: gen-api +gen-api: # 格式優化 + goctl api go -api ./generate/api/gateway.api -dir . -style go_zero diff --git a/etc/gateway.yaml b/etc/gateway.yaml index 7c2314c..283ff76 100644 --- a/etc/gateway.yaml +++ b/etc/gateway.yaml @@ -17,6 +17,7 @@ PermissionRpc: - 127.0.0.1:2379 Key: permission.rpc Token: + Secret: kupiHowBonBon Expired: 300s RedisCluster: diff --git a/gateway.json b/gateway.json index 90a7716..5ca67a2 100644 --- a/gateway.json +++ b/gateway.json @@ -42,6 +42,24 @@ } }, "parameters": [ + { + "name": "device_id", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "ip_address", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "brewser", + "in": "header", + "required": true, + "type": "string" + }, { "name": "body", "in": "body", @@ -112,7 +130,7 @@ "post": { "summary": "發送忘記密碼驗證", "description": "發送忘記密碼驗證(三分鐘內只能發一次信)", - "operationId": "ForgetPassworCode", + "operationId": "ForgetPasswordCode", "responses": { "200": { "description": "A successful response.", @@ -179,12 +197,6 @@ } }, "parameters": [ - { - "name": "uid", - "in": "header", - "required": true, - "type": "string" - }, { "name": "token", "in": "header", @@ -223,6 +235,24 @@ } }, "parameters": [ + { + "name": "device_id", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "ip_address", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "brewser", + "in": "header", + "required": true, + "type": "string" + }, { "name": "body", "in": "body", @@ -269,16 +299,74 @@ }, "parameters": [ { - "name": "uid", + "name": "token", + "in": "header", + "required": true, + "type": "string" + } + ], + "tags": [ + "gateway/member" + ] + } + }, + "/api/v1/member/refresh_access_token": { + "put": { + "summary": "更新Token", + "description": "用 RefreshToken 換取 AccessToken", + "operationId": "RefreshAccessToken", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/LoginResp" + } + }, + "400": { + "description": "輸入的參數錯誤", + "schema": { + "$ref": "#/definitions/BaseResponse" + } + }, + "403": { + "description": "無效的驗證碼", + "schema": { + "$ref": "#/definitions/BaseResponse" + } + }, + "500": { + "description": "伺服器出錯", + "schema": { + "$ref": "#/definitions/BaseResponse" + } + } + }, + "parameters": [ + { + "name": "device_id", "in": "header", "required": true, "type": "string" }, { - "name": "token", + "name": "ip_address", "in": "header", "required": true, "type": "string" + }, + { + "name": "brewser", + "in": "header", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateTokenReq" + } } ], "tags": [ @@ -418,13 +506,14 @@ "description": "平台名稱 digimon, google, twitter" }, "account_type": { - "type": "string", + "type": "integer", + "format": "int64", "enum": [ "1", "2", "3" ], - "description": "帳號類型 1 Email 2. 台灣手機 3. 任意" + "description": "帳號類型 1 手機 2 信箱 3 自定義帳號" } }, "title": "CreateAccountRequest", @@ -461,13 +550,14 @@ "description": "帳號名稱" }, "account_type": { - "type": "string", + "type": "integer", + "format": "int32", "enum": [ "1", "2", "3" ], - "description": "帳號類型 1 Email 2. 台灣手機 3. 任意" + "description": "帳號類型 1 手機 2 信箱 3 自定義帳號" } }, "title": "ForgetPasswordCodeReq", @@ -476,6 +566,11 @@ "account_type" ] }, + "GetMemberHeader": { + "type": "object", + "properties": {}, + "title": "GetMemberHeader" + }, "Header": { "type": "object", "properties": {}, @@ -484,6 +579,10 @@ "LoginItem": { "type": "object", "properties": { + "uid": { + "type": "string", + "description": "Account" + }, "access_token": { "type": "string", "description": "訪問令牌 預設 5 分鐘過期" @@ -499,6 +598,7 @@ }, "title": "LoginItem", "required": [ + "uid", "access_token", "refresh_token", "token_type" @@ -525,13 +625,14 @@ "description": "平台名稱 digimon, google, twitter" }, "account_type": { - "type": "string", + "type": "integer", + "format": "int64", "enum": [ "1", "2", "3" ], - "description": "帳號類型 1 Email 2. 台灣手機 3. 任意" + "description": "帳號類型 1 手機 2 信箱 3 自定義帳號" } }, "title": "LoginReq", @@ -559,6 +660,11 @@ "data" ] }, + "MemberLoginHeader": { + "type": "object", + "properties": {}, + "title": "MemberLoginHeader" + }, "Status": { "type": "object", "properties": { @@ -614,10 +720,70 @@ "token_check" ] }, + "UpdateTokenReq": { + "type": "object", + "properties": { + "uid": { + "type": "string", + "description": "uid" + }, + "token": { + "type": "string", + "description": "refresh token" + } + }, + "title": "UpdateTokenReq", + "required": [ + "uid", + "token" + ] + }, "UserInfo": { "type": "object", - "properties": {}, - "title": "UserInfo" + "properties": { + "uid": { + "type": "string" + }, + "verify_type": { + "type": "string" + }, + "alarm_type": { + "type": "string" + }, + "status": { + "type": "string" + }, + "language": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "curreate_time": { + "type": "string" + }, + "update_time": { + "type": "string" + }, + "nick_name": { + "type": "string" + } + }, + "title": "UserInfo", + "required": [ + "uid", + "verify_type", + "alarm_type", + "status", + "language", + "currency", + "avatar", + "curreate_time", + "update_time" + ] }, "UserInfoResp": { "type": "object", diff --git a/generate/api/member.api b/generate/api/member.api index 3cbf770..3e6acb7 100644 --- a/generate/api/member.api +++ b/generate/api/member.api @@ -166,12 +166,28 @@ type Header { Token string `header:"token"` } +type GetMemberHeader { + Token string `header:"token"` +} + type UserInfoResp { Status Status `json:"status"` // 狀態 Data UserInfo `json:"data"` } -type UserInfo {} +type UserInfo { + UID string `json:"uid"` + VerifyType string `json:"verify_type"` + AlarmType string `json:"alarm_type"` + Status string `json:"status"` + Language string `json:"language"` + Currency string `json:"currency"` + Avatar string `json:"avatar"` + CreateTime string `json:"curreate_time"` + UpdateTime string `json:"update_time"` + NickName *string `json:"nick_name,omitempty"` +} + @server( group: member @@ -189,7 +205,7 @@ service gateway { summary: "會員登出" ) @handler Logout - get /member/logout (Header) returns (BaseResponse) + get /member/logout (GetMemberHeader) returns (BaseResponse) /* @respdoc-400 (BaseResponse) // 輸入的參數錯誤 */ /* @respdoc-403 (BaseResponse) // 無效的Token */ @@ -198,5 +214,5 @@ service gateway { summary: "取得會員資訊" ) @handler Info - get /member/info (Header) returns (UserInfoResp) + get /member/info (GetMemberHeader) returns (UserInfoResp) } diff --git a/go.mod b/go.mod index e5d7c1f..c672259 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.3 require ( code.30cm.net/digimon/library-go/errors v1.0.1 + code.30cm.net/digimon/library-go/jwt v1.0.0 code.30cm.net/digimon/proto-all v0.0.0-20240826070029-4a87e93fd2cf github.com/gogo/protobuf v1.3.2 github.com/matcornic/hermes/v2 v2.1.0 diff --git a/internal/config/config.go b/internal/config/config.go index 0c2a47d..d0b19f6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ import ( type Config struct { rest.RestConf Token struct { + Secret string Expired time.Duration } // Redis Cluster diff --git a/internal/domain/payload.go b/internal/domain/payload.go new file mode 100644 index 0000000..0bd03fc --- /dev/null +++ b/internal/domain/payload.go @@ -0,0 +1,14 @@ +package domain + +type ContextKey string + +func (c ContextKey) ToString() string { + return string(c) +} + +const ( + RoleCode ContextKey = "role" + DeviceIDCode ContextKey = "device_id" + ScopeCode ContextKey = "scope" + UidCode ContextKey = "uid" +) diff --git a/internal/handler/member/info_handler.go b/internal/handler/member/info_handler.go index 8509d8b..6463664 100644 --- a/internal/handler/member/info_handler.go +++ b/internal/handler/member/info_handler.go @@ -14,7 +14,7 @@ import ( func InfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.Header + var req types.GetMemberHeader if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return diff --git a/internal/handler/member/logout_handler.go b/internal/handler/member/logout_handler.go index e8837f0..8eead1d 100644 --- a/internal/handler/member/logout_handler.go +++ b/internal/handler/member/logout_handler.go @@ -14,7 +14,7 @@ import ( func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req types.Header + var req types.GetMemberHeader if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return diff --git a/internal/logic/member/check_verify_code_logic.go b/internal/logic/member/check_verify_code_logic.go index fb25ae3..d6be5aa 100644 --- a/internal/logic/member/check_verify_code_logic.go +++ b/internal/logic/member/check_verify_code_logic.go @@ -4,11 +4,12 @@ import ( "app-cloudep-portal-api-gateway/internal/domain" "app-cloudep-portal-api-gateway/internal/svc" "app-cloudep-portal-api-gateway/internal/types" - ers "code.30cm.net/digimon/library-go/errors" - accountRpc "code.30cm.net/digimon/proto-all/pkg/member" "context" "fmt" + ers "code.30cm.net/digimon/library-go/errors" + accountRpc "code.30cm.net/digimon/proto-all/pkg/member" + "github.com/zeromicro/go-zero/core/logx" ) diff --git a/internal/logic/member/forget_password_code_logic.go b/internal/logic/member/forget_password_code_logic.go index 95d2371..bd0b7e3 100644 --- a/internal/logic/member/forget_password_code_logic.go +++ b/internal/logic/member/forget_password_code_logic.go @@ -4,10 +4,13 @@ import ( "app-cloudep-portal-api-gateway/internal/domain" "app-cloudep-portal-api-gateway/internal/svc" "app-cloudep-portal-api-gateway/internal/types" - ers "code.30cm.net/digimon/library-go/errors" - accountRpc "code.30cm.net/digimon/proto-all/pkg/member" "context" "fmt" + + notificationRpc "code.30cm.net/digimon/proto-all/pkg/notification" + + ers "code.30cm.net/digimon/library-go/errors" + accountRpc "code.30cm.net/digimon/proto-all/pkg/member" "github.com/matcornic/hermes/v2" "github.com/zeromicro/go-zero/core/logx" @@ -62,28 +65,27 @@ func (l *ForgetPasswordCodeLogic) ForgetPasswordCode(req *types.ForgetPasswordCo if err != nil { return nil, err } - fmt.Println(info) - // // 準備驗證碼郵件 - // nickName := info.Data.Uid - // if info.Data.NickName != nil { - // nickName = *info.Data.NickName - // } - // mailContent, title, err := ForgerZHTW(nickName, code.Data.VerifyCode) - // if err != nil { - // return nil, ers.InvalidFormat("failed to generate mail content: ", err.Error()) - // } + // 準備驗證碼郵件 + nickName := info.Data.Uid + if info.Data.NickName != nil { + nickName = *info.Data.NickName + } + mailContent, title, err := ForgerZHTW(nickName, code.Data.VerifyCode) + if err != nil { + return nil, ers.InvalidFormat("failed to generate mail content: ", err.Error()) + } - // // 發送郵件 - // _, err = l.svcCtx.NotificationRpc.SendMail(l.ctx, ¬ificationRpc.SendMailReq{ - // Body: mailContent, - // Subject: title, - // To: req.Account, - // From: l.svcCtx.Config.MailSender, - // }) - // if err != nil { - // return nil, err - // } + // 發送郵件 + _, err = l.svcCtx.NotificationRpc.SendMail(l.ctx, ¬ificationRpc.SendMailReq{ + Body: mailContent, + Subject: title, + To: req.Account, + From: l.svcCtx.Config.MailSender, + }) + if err != nil { + return nil, err + } // 設置 Redis 鍵,並設置 3 分鐘的過期時間 err = l.svcCtx.Redis.Set(rk, code.Data.VerifyCode) diff --git a/internal/logic/member/info_logic.go b/internal/logic/member/info_logic.go index 86c4f25..f8df1d9 100644 --- a/internal/logic/member/info_logic.go +++ b/internal/logic/member/info_logic.go @@ -1,11 +1,14 @@ package member import ( - "context" - + "app-cloudep-portal-api-gateway/internal/domain" + "app-cloudep-portal-api-gateway/internal/payload" "app-cloudep-portal-api-gateway/internal/svc" "app-cloudep-portal-api-gateway/internal/types" + "context" + "time" + accountRpc "code.30cm.net/digimon/proto-all/pkg/member" "github.com/zeromicro/go-zero/core/logx" ) @@ -23,8 +26,30 @@ func NewInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *InfoLogic { } } -func (l *InfoLogic) Info(req *types.Header) (resp *types.UserInfoResp, err error) { - // todo: add your logic here and delete this line +func (l *InfoLogic) Info(_ *types.GetMemberHeader) (resp *types.UserInfoResp, err error) { + info, err := l.svcCtx.AccountRpc.GetUserInfo(l.ctx, &accountRpc.GetUserInfoReq{ + Uid: payload.UID(l.ctx), + }) + if err != nil { + return nil, err + } - return + return &types.UserInfoResp{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMsg, + }, + Data: types.UserInfo{ + UID: info.Data.Uid, + VerifyType: info.Data.VerifyType.String(), + AlarmType: info.Data.AlarmType.String(), + Status: info.Data.Status.String(), + Language: info.Data.Language, + Currency: info.Data.Currency, + Avatar: info.Data.Avatar, + CreateTime: time.Unix(info.Data.CreateTime, 0).UTC().Format(time.RFC3339), + UpdateTime: time.Unix(info.Data.UpdateTime, 0).UTC().Format(time.RFC3339), + NickName: info.Data.NickName, + }, + }, nil } diff --git a/internal/logic/member/logout_logic.go b/internal/logic/member/logout_logic.go index f8a62d4..7a8a27b 100644 --- a/internal/logic/member/logout_logic.go +++ b/internal/logic/member/logout_logic.go @@ -1,8 +1,11 @@ package member import ( + "app-cloudep-portal-api-gateway/internal/domain" "context" + "code.30cm.net/digimon/proto-all/pkg/permission" + "app-cloudep-portal-api-gateway/internal/svc" "app-cloudep-portal-api-gateway/internal/types" @@ -23,8 +26,18 @@ func NewLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutLogi } } -func (l *LogoutLogic) Logout(req *types.Header) (resp *types.BaseResponse, err error) { - // todo: add your logic here and delete this line +func (l *LogoutLogic) Logout(req *types.GetMemberHeader) (resp *types.BaseResponse, err error) { + _, err = l.svcCtx.TokenRpc.CancelToken(l.ctx, &permission.CancelTokenReq{ + Token: req.Token, + }) + if err != nil { + return nil, err + } - return + return &types.BaseResponse{ + Status: types.Status{ + Code: domain.SuccessCode, + Message: domain.SuccessMsg, + }, + }, nil } diff --git a/internal/logic/member/upadte_password_logic.go b/internal/logic/member/upadte_password_logic.go index c685f1a..32a80a3 100644 --- a/internal/logic/member/upadte_password_logic.go +++ b/internal/logic/member/upadte_password_logic.go @@ -2,11 +2,12 @@ package member import ( "app-cloudep-portal-api-gateway/internal/domain" + "context" + "fmt" + ers "code.30cm.net/digimon/library-go/errors" accountRpc "code.30cm.net/digimon/proto-all/pkg/member" permissionRpc "code.30cm.net/digimon/proto-all/pkg/permission" - "context" - "fmt" "app-cloudep-portal-api-gateway/internal/svc" "app-cloudep-portal-api-gateway/internal/types" diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go index e9ed61e..1389a91 100644 --- a/internal/middleware/auth_middleware.go +++ b/internal/middleware/auth_middleware.go @@ -1,19 +1,79 @@ package middleware -import "net/http" +import ( + "app-cloudep-portal-api-gateway/internal/domain" + "app-cloudep-portal-api-gateway/internal/types" + "context" + "net/http" + + ers "code.30cm.net/digimon/library-go/errors" + token "code.30cm.net/digimon/library-go/jwt" + permissionRpc "code.30cm.net/digimon/proto-all/pkg/permission" + "github.com/zeromicro/go-zero/rest/httpx" +) + +type AuthMiddlewareParam struct { + TokenSec string + TokenClient permissionRpc.TokenServiceClient +} type AuthMiddleware struct { + tokenSec string + tokenClient permissionRpc.TokenServiceClient } -func NewAuthMiddleware() *AuthMiddleware { - return &AuthMiddleware{} -} - -func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // TODO generate middleware implement function, delete after code implementation - - // Passthrough to next handler if need - next(w, r) +func NewAuthMiddleware(param AuthMiddlewareParam) *AuthMiddleware { + return &AuthMiddleware{ + tokenSec: param.TokenSec, + tokenClient: param.TokenClient, } } + +// Handle 處理 Auth Middleware +func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 解析 Header + header := types.GetMemberHeader{} + if err := httpx.ParseHeaders(r, &header); err != nil { + m.writeErrorResponse(w, r, http.StatusBadRequest, "Failed to parse headers", int64(ers.InvalidFormat().FullCode())) + return + } + + // 驗證 Token + claim, err := token.ParseClaims(header.Token, m.tokenSec, true) + if err != nil { + // 是否需要紀錄錯誤,是不是只要紀錄除了驗證失敗或過期之外的真錯誤 + m.writeErrorResponse(w, r, http.StatusForbidden, err.Error(), int64(ers.Forbidden().FullCode())) + return + } + + // 驗證 Token 是否在黑名單中 + if _, err := m.tokenClient.ValidationToken(r.Context(), &permissionRpc.ValidationTokenReq{Token: header.Token}); err != nil { + m.writeErrorResponse(w, r, http.StatusForbidden, err.Error(), int64(ers.Forbidden().FullCode())) + return + } + + // 設置 context 並傳遞給下一個處理器 + ctx := SetContext(r, claim) + next(w, r.WithContext(ctx)) + } +} + +func SetContext(r *http.Request, claim token.DataClaims) context.Context { + ctx := context.WithValue(r.Context(), domain.RoleCode, claim.Role()) + ctx = context.WithValue(ctx, domain.UidCode, claim.UID()) + ctx = context.WithValue(ctx, domain.DeviceIDCode, claim.DeviceID()) + ctx = context.WithValue(ctx, domain.ScopeCode, claim.Get(domain.ScopeCode.ToString())) + + return ctx +} + +// writeErrorResponse 用於處理錯誤回應 +func (m *AuthMiddleware) writeErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int, message string, code int64) { + httpx.WriteJsonCtx(r.Context(), w, statusCode, types.BaseResponse{ + Status: types.Status{ + Code: code, + Message: message, + }, + }) +} diff --git a/internal/payload/token.go b/internal/payload/token.go new file mode 100644 index 0000000..6568540 --- /dev/null +++ b/internal/payload/token.go @@ -0,0 +1,22 @@ +package payload + +import ( + "app-cloudep-portal-api-gateway/internal/domain" + "context" +) + +func UID(ctx context.Context) string { + return ctx.Value(domain.UidCode).(string) +} + +func Scope(ctx context.Context) string { + return ctx.Value(domain.ScopeCode).(string) +} + +func Role(ctx context.Context) string { + return ctx.Value(domain.RoleCode).(string) +} + +func DeviceID(ctx context.Context) string { + return ctx.Value(domain.DeviceIDCode).(string) +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 1c80712..23d2610 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -32,11 +32,15 @@ func NewServiceContext(c config.Config) *ServiceContext { panic(err) } + tc := permissionRpc.NewTokenServiceClient(zrpc.MustNewClient(c.PermissionRpc).Conn()) return &ServiceContext{ - Config: c, - AuthMiddleware: middleware.NewAuthMiddleware(), + Config: c, + AuthMiddleware: middleware.NewAuthMiddleware(middleware.AuthMiddlewareParam{ + TokenSec: c.Token.Secret, + TokenClient: tc, + }), AccountRpc: accountRpc.NewAccountClient(zrpc.MustNewClient(c.AccountRpc).Conn()), - TokenRpc: permissionRpc.NewTokenServiceClient(zrpc.MustNewClient(c.PermissionRpc).Conn()), + TokenRpc: tc, NotificationRpc: notificationRpc.NewSenderServiceClient(zrpc.MustNewClient(c.NotificationRpc).Conn()), Redis: *newRedis, } diff --git a/internal/types/types.go b/internal/types/types.go index af6a3cf..b107c37 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -87,10 +87,24 @@ type Header struct { Token string `header:"token"` } +type GetMemberHeader struct { + Token string `header:"token"` +} + type UserInfoResp struct { Status Status `json:"status"` // 狀態 Data UserInfo `json:"data"` } type UserInfo struct { + UID string `json:"uid"` + VerifyType string `json:"verify_type"` + AlarmType string `json:"alarm_type"` + Status string `json:"status"` + Language string `json:"language"` + Currency string `json:"currency"` + Avatar string `json:"avatar"` + CreateTime string `json:"curreate_time"` + UpdateTime string `json:"update_time"` + NickName *string `json:"nick_name,omitempty"` }