feature/member #1

Merged
daniel.w merged 4 commits from feature/member into main 2024-08-27 07:40:29 +00:00
18 changed files with 423 additions and 73 deletions
Showing only changes of commit 66c85dd650 - Show all commits

View File

@ -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)
$(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

View File

@ -17,6 +17,7 @@ PermissionRpc:
- 127.0.0.1:2379
Key: permission.rpc
Token:
Secret: kupiHowBonBon
Expired: 300s
RedisCluster:

View File

@ -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",

View File

@ -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)
}

1
go.mod
View File

@ -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

View File

@ -11,6 +11,7 @@ import (
type Config struct {
rest.RestConf
Token struct {
Secret string
Expired time.Duration
}
// Redis Cluster

View File

@ -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"
)

View File

@ -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

View File

@ -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

View File

@ -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"
)

View File

@ -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, &notificationRpc.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, &notificationRpc.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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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"

View File

@ -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,
},
})
}

22
internal/payload/token.go Normal file
View File

@ -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)
}

View File

@ -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,
}

View File

@ -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"`
}