feat: add token func

This commit is contained in:
王性驊 2025-02-13 19:06:51 +08:00
parent 0e7f0a2b68
commit e8c5616206
32 changed files with 2007 additions and 108 deletions

View File

@ -6,7 +6,7 @@ GOFMT ?= gofmt "-s"
GOFILES := $(shell find . -name "*.go")
LDFLAGS := -s -w
VERSION="v1.0.1"
DOCKER_REPO="igs170911/permission"
DOCKER_REPO="code.30cm.net/permission"
.PHONY: test
test: # 進行測試
@ -46,24 +46,10 @@ build-docker:
rm -rf Dockerfile
@echo "Generate core-api files successfully"
.PHONY: gen-my-sql-model-up
gen-my-sql-model: # 建立 rpc 資料庫
goctl model mysql ddl -c no -s ./generate/database/mysql/20240816014305_create_permission_table.up.sql --style $(GO_ZERO_STYLE) -d ./internal/model -i ''
goctl model mysql ddl -c no -s ./generate/database/mysql/20240819013052_create_roles_table.up.sql --style $(GO_ZERO_STYLE) -d ./internal/model -i ''
goctl model mysql ddl -c no -s ./generate/database/mysql/20240819022436_create_user_role_table.up.sql --style $(GO_ZERO_STYLE) -d ./internal/model -i ''
goctl model mysql ddl -c no -s ./generate/database/mysql/20240819090248_create_role_permission_table.up.sql --style $(GO_ZERO_STYLE) -d ./internal/model -i ''
@echo "Generate mysql model files successfully"
.PHONY: mock-gen
mock-gen: # 建立 mock 資料
mockgen -source=./internal/model/permission_model.go -destination=./internal/mock/model/permission_model.go -package=mock
mockgen -source=./internal/model/permission_model_gen.go -destination=./internal/mock/model/permission_model_gen.go -package=mock
mockgen -source=./internal/model/role_model.go -destination=./internal/mock/model/role_model.go -package=mock
mockgen -source=./internal/model/role_model_gen.go -destination=./internal/mock/model/role_model_gen.go -package=mock
mockgen -source=./internal/model/role_permission_model.go -destination=./internal/mock/model/role_permission_model.go -package=mock
mockgen -source=./internal/model/role_permission_model_gen.go -destination=./internal/mock/model/role_permission_model_gen.go -package=mock
mockgen -source=./internal/model/user_role_model.go -destination=./internal/mock/model/user_role_model.go -package=mock
mockgen -source=./internal/model/user_role_model_gen.go -destination=./internal/mock/model/user_role_model_gen.go -package=mock
mockgen -source=./pkg/domain/repository/token.go -destination=./pkg/mock/repository/token.go -package=mock
@echo "Generate mock files successfully"

View File

@ -2,7 +2,7 @@
# BUILDER #
###########
FROM golang:1.23.4 AS builder
FROM golang:1.24.0 AS builder
ARG VERSION
ARG BUILT

View File

@ -2,7 +2,7 @@ syntax = "proto3";
package permission;
option go_package="./app-cloudep-permission-server";
option go_package="./permission";
// OKResp
message OKResp {}

7
go.mod
View File

@ -1,11 +1,16 @@
module code.30cm.net/digimon/app-cloudep-permission-server
go 1.23.4
go 1.23.6
require (
code.30cm.net/digimon/library-go/errs v1.2.14
github.com/alicebob/miniredis/v2 v2.34.0
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/pkg/errors v0.9.1
github.com/segmentio/ksuid v1.0.4
github.com/stretchr/testify v1.10.0
github.com/zeromicro/go-zero v1.8.0
go.uber.org/mock v0.5.0
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.5
)

12
go.sum
View File

@ -1,3 +1,5 @@
code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU=
code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
@ -46,6 +48,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@ -123,6 +127,8 @@ github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -183,6 +189,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
@ -230,8 +238,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewCancelOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext)
}
// CancelOneTimeToken 取消一次性使用
func (l *CancelOneTimeTokenLogic) CancelOneTimeToken(in *app_cloudep_permission_server.CancelOneTimeTokenReq) (*app_cloudep_permission_server.OKResp, error) {
func (l *CancelOneTimeTokenLogic) CancelOneTimeToken(in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.OKResp{}, nil
return &permission.OKResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewCancelTokenByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceConte
}
// CancelTokenByDeviceId 取消 Token 從 Device 視角出發可以選登出這個Device 下所有 token 登出這個Device 下指定token
func (l *CancelTokenByDeviceIdLogic) CancelTokenByDeviceId(in *app_cloudep_permission_server.DoTokenByDeviceIDReq) (*app_cloudep_permission_server.OKResp, error) {
func (l *CancelTokenByDeviceIdLogic) CancelTokenByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.OKResp{}, nil
return &permission.OKResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewCancelTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cance
}
// CancelToken 取消 Token也包含他裡面的 One Time Toke
func (l *CancelTokenLogic) CancelToken(in *app_cloudep_permission_server.CancelTokenReq) (*app_cloudep_permission_server.OKResp, error) {
func (l *CancelTokenLogic) CancelToken(in *permission.CancelTokenReq) (*permission.OKResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.OKResp{}, nil
return &permission.OKResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewCancelTokensLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Canc
}
// CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出
func (l *CancelTokensLogic) CancelTokens(in *app_cloudep_permission_server.DoTokenByUIDReq) (*app_cloudep_permission_server.OKResp, error) {
func (l *CancelTokensLogic) CancelTokens(in *permission.DoTokenByUIDReq) (*permission.OKResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.OKResp{}, nil
return &permission.OKResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewGetUserTokensByDeviceIdLogic(ctx context.Context, svcCtx *svc.ServiceCon
}
// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens
func (l *GetUserTokensByDeviceIdLogic) GetUserTokensByDeviceId(in *app_cloudep_permission_server.DoTokenByDeviceIDReq) (*app_cloudep_permission_server.Tokens, error) {
func (l *GetUserTokensByDeviceIdLogic) GetUserTokensByDeviceId(in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.Tokens{}, nil
return &permission.Tokens{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewGetUserTokensByUidLogic(ctx context.Context, svcCtx *svc.ServiceContext)
}
// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens
func (l *GetUserTokensByUidLogic) GetUserTokensByUid(in *app_cloudep_permission_server.QueryTokenByUIDReq) (*app_cloudep_permission_server.Tokens, error) {
func (l *GetUserTokensByUidLogic) GetUserTokensByUid(in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.Tokens{}, nil
return &permission.Tokens{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewNewOneTimeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *N
}
// NewOneTimeToken 建立一次性使用例如RefreshToken
func (l *NewOneTimeTokenLogic) NewOneTimeToken(in *app_cloudep_permission_server.CreateOneTimeTokenReq) (*app_cloudep_permission_server.CreateOneTimeTokenResp, error) {
func (l *NewOneTimeTokenLogic) NewOneTimeToken(in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.CreateOneTimeTokenResp{}, nil
return &permission.CreateOneTimeTokenResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewNewTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NewToken
}
// NewToken 建立一個新的 Token例如AccessToken
func (l *NewTokenLogic) NewToken(in *app_cloudep_permission_server.AuthorizationReq) (*app_cloudep_permission_server.TokenResp, error) {
func (l *NewTokenLogic) NewToken(in *permission.AuthorizationReq) (*permission.TokenResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.TokenResp{}, nil
return &permission.TokenResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Refr
}
// RefreshToken 更新目前的token 以及裡面包含的一次性 Token
func (l *RefreshTokenLogic) RefreshToken(in *app_cloudep_permission_server.RefreshTokenReq) (*app_cloudep_permission_server.RefreshTokenResp, error) {
func (l *RefreshTokenLogic) RefreshToken(in *permission.RefreshTokenReq) (*permission.RefreshTokenResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.RefreshTokenResp{}, nil
return &permission.RefreshTokenResp{}, nil
}

View File

@ -3,7 +3,7 @@ package tokenservicelogic
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
@ -24,8 +24,8 @@ func NewValidationTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *V
}
// ValidationToken 驗證這個 Token 有沒有效
func (l *ValidationTokenLogic) ValidationToken(in *app_cloudep_permission_server.ValidationTokenReq) (*app_cloudep_permission_server.ValidationTokenResp, error) {
func (l *ValidationTokenLogic) ValidationToken(in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) {
// todo: add your logic here and delete this line
return &app_cloudep_permission_server.ValidationTokenResp{}, nil
return &permission.ValidationTokenResp{}, nil
}

View File

@ -7,14 +7,15 @@ package server
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/logic/tokenservice"
tokenservicelogic "code.30cm.net/digimon/app-cloudep-permission-server/internal/logic/tokenservice"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
)
type TokenServiceServer struct {
svcCtx *svc.ServiceContext
app_cloudep_permission_server.UnimplementedTokenServiceServer
permission.UnimplementedTokenServiceServer
}
func NewTokenServiceServer(svcCtx *svc.ServiceContext) *TokenServiceServer {
@ -24,61 +25,61 @@ func NewTokenServiceServer(svcCtx *svc.ServiceContext) *TokenServiceServer {
}
// NewToken 建立一個新的 Token例如AccessToken
func (s *TokenServiceServer) NewToken(ctx context.Context, in *app_cloudep_permission_server.AuthorizationReq) (*app_cloudep_permission_server.TokenResp, error) {
func (s *TokenServiceServer) NewToken(ctx context.Context, in *permission.AuthorizationReq) (*permission.TokenResp, error) {
l := tokenservicelogic.NewNewTokenLogic(ctx, s.svcCtx)
return l.NewToken(in)
}
// RefreshToken 更新目前的token 以及裡面包含的一次性 Token
func (s *TokenServiceServer) RefreshToken(ctx context.Context, in *app_cloudep_permission_server.RefreshTokenReq) (*app_cloudep_permission_server.RefreshTokenResp, error) {
func (s *TokenServiceServer) RefreshToken(ctx context.Context, in *permission.RefreshTokenReq) (*permission.RefreshTokenResp, error) {
l := tokenservicelogic.NewRefreshTokenLogic(ctx, s.svcCtx)
return l.RefreshToken(in)
}
// CancelToken 取消 Token也包含他裡面的 One Time Toke
func (s *TokenServiceServer) CancelToken(ctx context.Context, in *app_cloudep_permission_server.CancelTokenReq) (*app_cloudep_permission_server.OKResp, error) {
func (s *TokenServiceServer) CancelToken(ctx context.Context, in *permission.CancelTokenReq) (*permission.OKResp, error) {
l := tokenservicelogic.NewCancelTokenLogic(ctx, s.svcCtx)
return l.CancelToken(in)
}
// ValidationToken 驗證這個 Token 有沒有效
func (s *TokenServiceServer) ValidationToken(ctx context.Context, in *app_cloudep_permission_server.ValidationTokenReq) (*app_cloudep_permission_server.ValidationTokenResp, error) {
func (s *TokenServiceServer) ValidationToken(ctx context.Context, in *permission.ValidationTokenReq) (*permission.ValidationTokenResp, error) {
l := tokenservicelogic.NewValidationTokenLogic(ctx, s.svcCtx)
return l.ValidationToken(in)
}
// CancelTokens 取消 Token 從UID 視角,以及 token id 視角出發, UID 登出,底下所有 Device ID 也要登出, Token ID 登出, 所有 UID + Device 都要登出
func (s *TokenServiceServer) CancelTokens(ctx context.Context, in *app_cloudep_permission_server.DoTokenByUIDReq) (*app_cloudep_permission_server.OKResp, error) {
func (s *TokenServiceServer) CancelTokens(ctx context.Context, in *permission.DoTokenByUIDReq) (*permission.OKResp, error) {
l := tokenservicelogic.NewCancelTokensLogic(ctx, s.svcCtx)
return l.CancelTokens(in)
}
// CancelTokenByDeviceId 取消 Token 從 Device 視角出發可以選登出這個Device 下所有 token 登出這個Device 下指定token
func (s *TokenServiceServer) CancelTokenByDeviceId(ctx context.Context, in *app_cloudep_permission_server.DoTokenByDeviceIDReq) (*app_cloudep_permission_server.OKResp, error) {
func (s *TokenServiceServer) CancelTokenByDeviceId(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.OKResp, error) {
l := tokenservicelogic.NewCancelTokenByDeviceIdLogic(ctx, s.svcCtx)
return l.CancelTokenByDeviceId(in)
}
// GetUserTokensByDeviceId 取得目前所對應的 DeviceID 所存在的 Tokens
func (s *TokenServiceServer) GetUserTokensByDeviceId(ctx context.Context, in *app_cloudep_permission_server.DoTokenByDeviceIDReq) (*app_cloudep_permission_server.Tokens, error) {
func (s *TokenServiceServer) GetUserTokensByDeviceId(ctx context.Context, in *permission.DoTokenByDeviceIDReq) (*permission.Tokens, error) {
l := tokenservicelogic.NewGetUserTokensByDeviceIdLogic(ctx, s.svcCtx)
return l.GetUserTokensByDeviceId(in)
}
// GetUserTokensByUid 取得目前所對應的 UID 所存在的 Tokens
func (s *TokenServiceServer) GetUserTokensByUid(ctx context.Context, in *app_cloudep_permission_server.QueryTokenByUIDReq) (*app_cloudep_permission_server.Tokens, error) {
func (s *TokenServiceServer) GetUserTokensByUid(ctx context.Context, in *permission.QueryTokenByUIDReq) (*permission.Tokens, error) {
l := tokenservicelogic.NewGetUserTokensByUidLogic(ctx, s.svcCtx)
return l.GetUserTokensByUid(in)
}
// NewOneTimeToken 建立一次性使用例如RefreshToken
func (s *TokenServiceServer) NewOneTimeToken(ctx context.Context, in *app_cloudep_permission_server.CreateOneTimeTokenReq) (*app_cloudep_permission_server.CreateOneTimeTokenResp, error) {
func (s *TokenServiceServer) NewOneTimeToken(ctx context.Context, in *permission.CreateOneTimeTokenReq) (*permission.CreateOneTimeTokenResp, error) {
l := tokenservicelogic.NewNewOneTimeTokenLogic(ctx, s.svcCtx)
return l.NewOneTimeToken(in)
}
// CancelOneTimeToken 取消一次性使用
func (s *TokenServiceServer) CancelOneTimeToken(ctx context.Context, in *app_cloudep_permission_server.CancelOneTimeTokenReq) (*app_cloudep_permission_server.OKResp, error) {
func (s *TokenServiceServer) CancelOneTimeToken(ctx context.Context, in *permission.CancelOneTimeTokenReq) (*permission.OKResp, error) {
l := tokenservicelogic.NewCancelOneTimeTokenLogic(ctx, s.svcCtx)
return l.CancelOneTimeToken(in)
}

View File

@ -4,7 +4,7 @@ import (
"flag"
"fmt"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/app-cloudep-permission-server"
"code.30cm.net/digimon/app-cloudep-permission-server/gen_result/pb/permission"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/config"
tokenserviceServer "code.30cm.net/digimon/app-cloudep-permission-server/internal/server/tokenservice"
"code.30cm.net/digimon/app-cloudep-permission-server/internal/svc"
@ -26,7 +26,7 @@ func main() {
ctx := svc.NewServiceContext(c)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
app_cloudep_permission_server.RegisterTokenServiceServer(grpcServer, tokenserviceServer.NewTokenServiceServer(ctx))
permission.RegisterTokenServiceServer(grpcServer, tokenserviceServer.NewTokenServiceServer(ctx))
if c.Mode == service.DevMode || c.Mode == service.TestMode {
reflection.Register(grpcServer)

View File

@ -0,0 +1,8 @@
package entity
import "github.com/golang-jwt/jwt/v4"
type Claims struct {
jwt.RegisteredClaims
Data interface{} `json:"data"`
}

View File

@ -1 +1,43 @@
package domain
import (
"fmt"
"strings"
ers "code.30cm.net/digimon/library-go/errs"
"code.30cm.net/digimon/library-go/errs/code"
"github.com/zeromicro/go-zero/core/logx"
)
const (
TokenServerErrorCode = 1 + iota
TokenServerRedisErrorCode
TokenValidateErrorCode
TokenClaimErrorCode
TokenCreateErrorCode
TokenRefreshErrorCode
TokenCancelErrorCode
TokensCancelErrorCode
TokenGetErrorCode
NewOneTokenErrorCode
DelOneTokenErrorCode
SendTooShortErrorCode
SetForgetPasswordRedisErrorCode
FailedToGetCorrectVerifyCode
SendVerifyCodeRedisErrorCode
GenerateVerifyCodeRedisErrorCode
FailedToCheckVerifyCode
AccountPlatformNotCorrectErrorCode
)
func TokenError(ec ers.ErrorCode, s ...string) *ers.LibError {
return ers.NewError(code.CloudEPPermission, code.SigAndPayloadNotMatched, ec.ToUint32(), fmt.Sprintf("token create error: %s", strings.Join(s, " ")))
}
func TokenErrorL(ec ers.ErrorCode,
l logx.Logger, filed []logx.LogField, s ...string) *ers.LibError {
e := TokenError(ec, s...)
l.WithCallerSkip(1).WithFields(filed...).Error(e.Error())
return e
}

View File

@ -1,9 +1,10 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"context"
"time"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
)
// TokenRepo 管理Token

View File

@ -0,0 +1,38 @@
package token
type Additional string
func (a Additional) String() string {
return string(a)
}
const (
ID Additional = "id"
Role Additional = "role"
Device Additional = "device"
UID Additional = "uid"
Account Additional = "account"
Scope Additional = "scope"
Type Additional = "token_type"
)
// 定義一個集合存放所有合法的 Additional Keys
var validAdditionalKeys = map[Additional]struct{}{
ID: {},
Role: {},
Device: {},
UID: {},
Account: {},
Scope: {},
Type: {},
}
// IsValidAdditional 檢查是否是有效的 Additional Key
func IsValidAdditional(key Additional) bool {
_, exists := validAdditionalKeys[key]
return exists
}
const (
Issuer = "permission"
)

View File

@ -0,0 +1,7 @@
package token
type TScope string
func (s *TScope) ToString() string {
return string(*s)
}

11
pkg/domain/token/type.go Normal file
View File

@ -0,0 +1,11 @@
package token
type VerifyType string
func (t *VerifyType) ToString() string {
return string(*t)
}
const (
Bearer VerifyType = "Bearer"
)

View File

@ -0,0 +1,10 @@
package usecase
import "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token"
// Additional 系統在 Token 當中的附加資訊
type Additional interface {
Set(key token.Additional, val string)
Get(key token.Additional) string
GetAll() map[string]string
}

View File

@ -1,40 +1,92 @@
package usecase
import "context"
import (
"context"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"github.com/golang-jwt/jwt/v4"
)
type TokenUseCase interface {
// NewToken 創建新 Token通常為 Access Token
NewToken(ctx context.Context, req AuthorizationReq) (TokenResp, error)
// RefreshToken 刷新目前的 Token包括一次性 Token
RefreshToken(ctx context.Context, req RefreshTokenReq) (RefreshTokenResp, error)
// CancelToken 取消 Token包括取消其關聯的 One-Time Token
CancelToken(ctx context.Context, req CancelTokenReq) error
// ValidationToken 驗證 Token 是否有效
ValidationToken(ctx context.Context, req ValidationTokenReq) (ValidationTokenResp, error)
// CancelTokens 根據 UID 或 Token ID 取消所有相關 Token通常在用戶登出時使用
CancelTokens(ctx context.Context, req DoTokenByUIDReq) error
// CancelTokenByDeviceID 根據 Device ID 取消所有相關的 Token
CancelTokenByDeviceID(ctx context.Context, req DoTokenByDeviceIDReq) error
// GetUserTokensByDeviceID 根據 Device ID 獲取所有 Token
GetUserTokensByDeviceID(ctx context.Context, req DoTokenByDeviceIDReq) ([]*TokenResp, error)
// GetUserTokensByUID 根據 UID 獲取所有 Token
GetUserTokensByUID(ctx context.Context, req QueryTokenByUIDReq) ([]*TokenResp, error)
// NewOneTimeToken 創建一次性 Token例如 Refresh Token
NewOneTimeToken(ctx context.Context, req CreateOneTimeTokenReq) (CreateOneTimeTokenResp, error)
// CancelOneTimeToken 取消一次性 Token
CancelOneTimeToken(ctx context.Context, req CancelOneTimeTokenReq) error
ParseClaims
// GenerateAccessToken 產生新的 Access Token
GenerateAccessToken(ctx context.Context, req GenerateTokenRequest) (AccessTokenResponse, error)
// RefreshAccessToken 使用 Refresh Token 更新 Access Token刷新令牌
RefreshAccessToken(ctx context.Context, req RefreshTokenRequest) (RefreshTokenResponse, error)
// RevokeToken 撤銷單個 Token
RevokeToken(ctx context.Context, req TokenRequest) error
// VerifyToken 驗證 Token 是否有效
VerifyToken(ctx context.Context, req TokenRequest) (VerifyTokenResponse, error)
// RevokeTokensByUID 根據 UID 撤銷所有 Token
RevokeTokensByUID(ctx context.Context, req RevokeTokensByUIDRequest) error
// RevokeTokensByDeviceID 根據 Device ID 取消所有相關的 Token
RevokeTokensByDeviceID(ctx context.Context, deviceID string) error
// GetUserTokensByDeviceID 根據 Device ID 獲取所有 AccessToken
GetUserTokensByDeviceID(ctx context.Context, deviceID string) ([]*AccessTokenResponse, error)
// GetUserTokensByUID 根據 UID 獲取所有 AccessToken
GetUserTokensByUID(ctx context.Context, uid string) ([]*AccessTokenResponse, error)
// ReadTokenBasicData 檢查Token 帶的資料
ReadTokenBasicData(ctx context.Context, token string) (map[string]string, error)
ReadTokenBasicData(ctx context.Context, token string) (Additional, error)
}
// AuthorizationReq 定義授權請求的結構
type AuthorizationReq struct {
GrantType string `json:"grant_type"` // 授權類型
type ParseClaims interface {
// CreateAccessToken 建立 access token
CreateAccessToken(token entity.Token, data any, secretKey string) (string, error)
// CreateRefreshToken 建立 RefreshToken
CreateRefreshToken(accessToken string) string
// ParseJWTClaimsByAccessToken 使用Access Token 解析出 JWT 資訊
ParseJWTClaimsByAccessToken(accessToken string, secret string, validate bool) (jwt.MapClaims, error)
// ParseSystemClaimsByAccessToken 使用Access Token 解析出 系統資訊
ParseSystemClaimsByAccessToken(accessToken string, secret string, validate bool) (map[string]string, error)
}
// GenerateTokenRequest 定義授權請求的結構
type GenerateTokenRequest struct {
TokenType string `json:"token_type"` // 告訴前端Token 類型
DeviceID string `json:"device_id"` // 設備 ID
Scope string `json:"scope"` // 授權範圍
Data map[string]string `json:"data"` // 附加數據
Expires int64 `json:"expires"` // 過期時間(秒)
IsRefreshToken bool `json:"is_refresh_token"` // 是否為刷新令牌
Expires int64 `json:"expires"` // 指定過期時間 UnixNano UTC 時間(沒給 = now 加設定秒數)
RefreshExpires int64 `json:"refresh_expires"` // 指定過期時間 UnixNano UTC 時間(沒給 = now 加設定秒數)
Role string `json:"role"` // 是否為刷新令牌
Account string `json:"account"` // 登入時的帳號
Account string `json:"account"` // 登入時用的帳號
UID string `json:"uid"` // 使用者在系統中的帳號
Data map[string]string `json:"data"` // 附加數據 -> 不在上面的以後要額外放進來的
}
// AccessTokenResponse 定義訪問令牌響應的結構
type AccessTokenResponse struct {
AccessToken string `json:"access_token"` // 訪問令牌
ExpiresIn int64 `json:"expires_in"` // 過期時間 UnixNano UTC 時間
RefreshToken string `json:"refresh_token"` // 刷新令牌
}
// RefreshTokenRequest 更新 Token 的請求
type RefreshTokenRequest struct {
Token string `json:"token"` // 令牌
Scope string `json:"scope"` // 授權範圍
Expires int64 `json:"expires"` // 指定過期時間 UnixNano UTC 時間(沒給 = now 加設定秒數)
RefreshExpires int64 `json:"refresh_expires"` // 指定過期時間 UnixNano UTC 時間(沒給 = now 加設定秒數)
DeviceID string `json:"device_id"` // 設備 ID
}
// RefreshTokenResponse 更新令牌的響應
type RefreshTokenResponse struct {
AccessToken string `json:"token"` // 新的訪問令牌
RefreshToken string `json:"refresh_token"` // 更新令牌
ExpiresIn int64 `json:"expires_in"` // 過期時間(秒)
TokenType string `json:"token_type"` // 令牌類型
}
type TokenRequest struct {
Token string `json:"token"` // 需要註銷的令牌
}
type VerifyTokenResponse struct {
Token entity.Token `json:"token"` // Token 詳情
Data map[string]string `json:"data"` // 附加資料
}
type RevokeTokensByUIDRequest struct {
IDs []string `json:"ids"` // Token ID 列表
UID string `json:"uid"` // 用戶 ID
}

View File

@ -0,0 +1,491 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./pkg/domain/repository/token.go
//
// Generated by this command:
//
// mockgen -source=./pkg/domain/repository/token.go -destination=./pkg/mock/repository/token.go -package=mock
//
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
time "time"
entity "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
gomock "go.uber.org/mock/gomock"
)
// MockTokenRepo is a mock of TokenRepo interface.
type MockTokenRepo struct {
ctrl *gomock.Controller
recorder *MockTokenRepoMockRecorder
isgomock struct{}
}
// MockTokenRepoMockRecorder is the mock recorder for MockTokenRepo.
type MockTokenRepoMockRecorder struct {
mock *MockTokenRepo
}
// NewMockTokenRepo creates a new mock instance.
func NewMockTokenRepo(ctrl *gomock.Controller) *MockTokenRepo {
mock := &MockTokenRepo{ctrl: ctrl}
mock.recorder = &MockTokenRepoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTokenRepo) EXPECT() *MockTokenRepoMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockTokenRepo) Create(ctx context.Context, token entity.Token) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, token)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
func (mr *MockTokenRepoMockRecorder) Create(ctx, token any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockTokenRepo)(nil).Create), ctx, token)
}
// CreateOneTimeToken mocks base method.
func (m *MockTokenRepo) CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, et time.Duration) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOneTimeToken", ctx, key, ticket, et)
ret0, _ := ret[0].(error)
return ret0
}
// CreateOneTimeToken indicates an expected call of CreateOneTimeToken.
func (mr *MockTokenRepoMockRecorder) CreateOneTimeToken(ctx, key, ticket, et any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOneTimeToken", reflect.TypeOf((*MockTokenRepo)(nil).CreateOneTimeToken), ctx, key, ticket, et)
}
// Delete mocks base method.
func (m *MockTokenRepo) Delete(ctx context.Context, token entity.Token) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, token)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockTokenRepoMockRecorder) Delete(ctx, token any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockTokenRepo)(nil).Delete), ctx, token)
}
// DeleteAccessTokenByID mocks base method.
func (m *MockTokenRepo) DeleteAccessTokenByID(ctx context.Context, ids []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessTokenByID", ctx, ids)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessTokenByID indicates an expected call of DeleteAccessTokenByID.
func (mr *MockTokenRepoMockRecorder) DeleteAccessTokenByID(ctx, ids any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokenByID", reflect.TypeOf((*MockTokenRepo)(nil).DeleteAccessTokenByID), ctx, ids)
}
// DeleteAccessTokensByDeviceID mocks base method.
func (m *MockTokenRepo) DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessTokensByDeviceID", ctx, deviceID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessTokensByDeviceID indicates an expected call of DeleteAccessTokensByDeviceID.
func (mr *MockTokenRepoMockRecorder) DeleteAccessTokensByDeviceID(ctx, deviceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokensByDeviceID", reflect.TypeOf((*MockTokenRepo)(nil).DeleteAccessTokensByDeviceID), ctx, deviceID)
}
// DeleteAccessTokensByUID mocks base method.
func (m *MockTokenRepo) DeleteAccessTokensByUID(ctx context.Context, uid string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessTokensByUID", ctx, uid)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessTokensByUID indicates an expected call of DeleteAccessTokensByUID.
func (mr *MockTokenRepoMockRecorder) DeleteAccessTokensByUID(ctx, uid any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokensByUID", reflect.TypeOf((*MockTokenRepo)(nil).DeleteAccessTokensByUID), ctx, uid)
}
// DeleteOneTimeToken mocks base method.
func (m *MockTokenRepo) DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOneTimeToken", ctx, ids, tokens)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteOneTimeToken indicates an expected call of DeleteOneTimeToken.
func (mr *MockTokenRepoMockRecorder) DeleteOneTimeToken(ctx, ids, tokens any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOneTimeToken", reflect.TypeOf((*MockTokenRepo)(nil).DeleteOneTimeToken), ctx, ids, tokens)
}
// GetAccessTokenByID mocks base method.
func (m *MockTokenRepo) GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenByID", ctx, id)
ret0, _ := ret[0].(entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenByID indicates an expected call of GetAccessTokenByID.
func (mr *MockTokenRepoMockRecorder) GetAccessTokenByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenByID", reflect.TypeOf((*MockTokenRepo)(nil).GetAccessTokenByID), ctx, id)
}
// GetAccessTokenByOneTimeToken mocks base method.
func (m *MockTokenRepo) GetAccessTokenByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenByOneTimeToken", ctx, oneTimeToken)
ret0, _ := ret[0].(entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenByOneTimeToken indicates an expected call of GetAccessTokenByOneTimeToken.
func (mr *MockTokenRepoMockRecorder) GetAccessTokenByOneTimeToken(ctx, oneTimeToken any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenByOneTimeToken", reflect.TypeOf((*MockTokenRepo)(nil).GetAccessTokenByOneTimeToken), ctx, oneTimeToken)
}
// GetAccessTokenCountByDeviceID mocks base method.
func (m *MockTokenRepo) GetAccessTokenCountByDeviceID(ctx context.Context, deviceID string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenCountByDeviceID", ctx, deviceID)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenCountByDeviceID indicates an expected call of GetAccessTokenCountByDeviceID.
func (mr *MockTokenRepoMockRecorder) GetAccessTokenCountByDeviceID(ctx, deviceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenCountByDeviceID", reflect.TypeOf((*MockTokenRepo)(nil).GetAccessTokenCountByDeviceID), ctx, deviceID)
}
// GetAccessTokenCountByUID mocks base method.
func (m *MockTokenRepo) GetAccessTokenCountByUID(ctx context.Context, uid string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenCountByUID", ctx, uid)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenCountByUID indicates an expected call of GetAccessTokenCountByUID.
func (mr *MockTokenRepoMockRecorder) GetAccessTokenCountByUID(ctx, uid any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenCountByUID", reflect.TypeOf((*MockTokenRepo)(nil).GetAccessTokenCountByUID), ctx, uid)
}
// GetAccessTokensByDeviceID mocks base method.
func (m *MockTokenRepo) GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokensByDeviceID", ctx, deviceID)
ret0, _ := ret[0].([]entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokensByDeviceID indicates an expected call of GetAccessTokensByDeviceID.
func (mr *MockTokenRepoMockRecorder) GetAccessTokensByDeviceID(ctx, deviceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokensByDeviceID", reflect.TypeOf((*MockTokenRepo)(nil).GetAccessTokensByDeviceID), ctx, deviceID)
}
// GetAccessTokensByUID mocks base method.
func (m *MockTokenRepo) GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokensByUID", ctx, uid)
ret0, _ := ret[0].([]entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokensByUID indicates an expected call of GetAccessTokensByUID.
func (mr *MockTokenRepoMockRecorder) GetAccessTokensByUID(ctx, uid any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokensByUID", reflect.TypeOf((*MockTokenRepo)(nil).GetAccessTokensByUID), ctx, uid)
}
// MockCreate is a mock of Create interface.
type MockCreate struct {
ctrl *gomock.Controller
recorder *MockCreateMockRecorder
isgomock struct{}
}
// MockCreateMockRecorder is the mock recorder for MockCreate.
type MockCreateMockRecorder struct {
mock *MockCreate
}
// NewMockCreate creates a new mock instance.
func NewMockCreate(ctrl *gomock.Controller) *MockCreate {
mock := &MockCreate{ctrl: ctrl}
mock.recorder = &MockCreateMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCreate) EXPECT() *MockCreateMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockCreate) Create(ctx context.Context, token entity.Token) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, token)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
func (mr *MockCreateMockRecorder) Create(ctx, token any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCreate)(nil).Create), ctx, token)
}
// CreateOneTimeToken mocks base method.
func (m *MockCreate) CreateOneTimeToken(ctx context.Context, key string, ticket entity.Ticket, et time.Duration) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOneTimeToken", ctx, key, ticket, et)
ret0, _ := ret[0].(error)
return ret0
}
// CreateOneTimeToken indicates an expected call of CreateOneTimeToken.
func (mr *MockCreateMockRecorder) CreateOneTimeToken(ctx, key, ticket, et any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOneTimeToken", reflect.TypeOf((*MockCreate)(nil).CreateOneTimeToken), ctx, key, ticket, et)
}
// MockGet is a mock of Get interface.
type MockGet struct {
ctrl *gomock.Controller
recorder *MockGetMockRecorder
isgomock struct{}
}
// MockGetMockRecorder is the mock recorder for MockGet.
type MockGetMockRecorder struct {
mock *MockGet
}
// NewMockGet creates a new mock instance.
func NewMockGet(ctrl *gomock.Controller) *MockGet {
mock := &MockGet{ctrl: ctrl}
mock.recorder = &MockGetMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGet) EXPECT() *MockGetMockRecorder {
return m.recorder
}
// GetAccessTokenByID mocks base method.
func (m *MockGet) GetAccessTokenByID(ctx context.Context, id string) (entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenByID", ctx, id)
ret0, _ := ret[0].(entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenByID indicates an expected call of GetAccessTokenByID.
func (mr *MockGetMockRecorder) GetAccessTokenByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenByID", reflect.TypeOf((*MockGet)(nil).GetAccessTokenByID), ctx, id)
}
// GetAccessTokenByOneTimeToken mocks base method.
func (m *MockGet) GetAccessTokenByOneTimeToken(ctx context.Context, oneTimeToken string) (entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenByOneTimeToken", ctx, oneTimeToken)
ret0, _ := ret[0].(entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenByOneTimeToken indicates an expected call of GetAccessTokenByOneTimeToken.
func (mr *MockGetMockRecorder) GetAccessTokenByOneTimeToken(ctx, oneTimeToken any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenByOneTimeToken", reflect.TypeOf((*MockGet)(nil).GetAccessTokenByOneTimeToken), ctx, oneTimeToken)
}
// GetAccessTokenCountByDeviceID mocks base method.
func (m *MockGet) GetAccessTokenCountByDeviceID(ctx context.Context, deviceID string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenCountByDeviceID", ctx, deviceID)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenCountByDeviceID indicates an expected call of GetAccessTokenCountByDeviceID.
func (mr *MockGetMockRecorder) GetAccessTokenCountByDeviceID(ctx, deviceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenCountByDeviceID", reflect.TypeOf((*MockGet)(nil).GetAccessTokenCountByDeviceID), ctx, deviceID)
}
// GetAccessTokenCountByUID mocks base method.
func (m *MockGet) GetAccessTokenCountByUID(ctx context.Context, uid string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokenCountByUID", ctx, uid)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokenCountByUID indicates an expected call of GetAccessTokenCountByUID.
func (mr *MockGetMockRecorder) GetAccessTokenCountByUID(ctx, uid any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokenCountByUID", reflect.TypeOf((*MockGet)(nil).GetAccessTokenCountByUID), ctx, uid)
}
// GetAccessTokensByDeviceID mocks base method.
func (m *MockGet) GetAccessTokensByDeviceID(ctx context.Context, deviceID string) ([]entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokensByDeviceID", ctx, deviceID)
ret0, _ := ret[0].([]entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokensByDeviceID indicates an expected call of GetAccessTokensByDeviceID.
func (mr *MockGetMockRecorder) GetAccessTokensByDeviceID(ctx, deviceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokensByDeviceID", reflect.TypeOf((*MockGet)(nil).GetAccessTokensByDeviceID), ctx, deviceID)
}
// GetAccessTokensByUID mocks base method.
func (m *MockGet) GetAccessTokensByUID(ctx context.Context, uid string) ([]entity.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccessTokensByUID", ctx, uid)
ret0, _ := ret[0].([]entity.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccessTokensByUID indicates an expected call of GetAccessTokensByUID.
func (mr *MockGetMockRecorder) GetAccessTokensByUID(ctx, uid any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccessTokensByUID", reflect.TypeOf((*MockGet)(nil).GetAccessTokensByUID), ctx, uid)
}
// MockDelete is a mock of Delete interface.
type MockDelete struct {
ctrl *gomock.Controller
recorder *MockDeleteMockRecorder
isgomock struct{}
}
// MockDeleteMockRecorder is the mock recorder for MockDelete.
type MockDeleteMockRecorder struct {
mock *MockDelete
}
// NewMockDelete creates a new mock instance.
func NewMockDelete(ctrl *gomock.Controller) *MockDelete {
mock := &MockDelete{ctrl: ctrl}
mock.recorder = &MockDeleteMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDelete) EXPECT() *MockDeleteMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockDelete) Delete(ctx context.Context, token entity.Token) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, token)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockDeleteMockRecorder) Delete(ctx, token any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDelete)(nil).Delete), ctx, token)
}
// DeleteAccessTokenByID mocks base method.
func (m *MockDelete) DeleteAccessTokenByID(ctx context.Context, ids []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessTokenByID", ctx, ids)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessTokenByID indicates an expected call of DeleteAccessTokenByID.
func (mr *MockDeleteMockRecorder) DeleteAccessTokenByID(ctx, ids any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokenByID", reflect.TypeOf((*MockDelete)(nil).DeleteAccessTokenByID), ctx, ids)
}
// DeleteAccessTokensByDeviceID mocks base method.
func (m *MockDelete) DeleteAccessTokensByDeviceID(ctx context.Context, deviceID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessTokensByDeviceID", ctx, deviceID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessTokensByDeviceID indicates an expected call of DeleteAccessTokensByDeviceID.
func (mr *MockDeleteMockRecorder) DeleteAccessTokensByDeviceID(ctx, deviceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokensByDeviceID", reflect.TypeOf((*MockDelete)(nil).DeleteAccessTokensByDeviceID), ctx, deviceID)
}
// DeleteAccessTokensByUID mocks base method.
func (m *MockDelete) DeleteAccessTokensByUID(ctx context.Context, uid string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessTokensByUID", ctx, uid)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessTokensByUID indicates an expected call of DeleteAccessTokensByUID.
func (mr *MockDeleteMockRecorder) DeleteAccessTokensByUID(ctx, uid any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessTokensByUID", reflect.TypeOf((*MockDelete)(nil).DeleteAccessTokensByUID), ctx, uid)
}
// DeleteOneTimeToken mocks base method.
func (m *MockDelete) DeleteOneTimeToken(ctx context.Context, ids []string, tokens []entity.Token) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOneTimeToken", ctx, ids, tokens)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteOneTimeToken indicates an expected call of DeleteOneTimeToken.
func (mr *MockDeleteMockRecorder) DeleteOneTimeToken(ctx, ids, tokens any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOneTimeToken", reflect.TypeOf((*MockDelete)(nil).DeleteOneTimeToken), ctx, ids, tokens)
}

View File

@ -1,15 +1,16 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/zeromicro/go-zero/core/stores/redis"
"time"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
"github.com/zeromicro/go-zero/core/stores/redis"
)
// TokenRepositoryParam token 需要的參數

View File

@ -1,16 +1,17 @@
package repository
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"context"
"encoding/json"
"errors"
"testing"
"time"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stores/redis"
"testing"
"time"
)
func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) {

35
pkg/usecase/additional.go Normal file
View File

@ -0,0 +1,35 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase"
)
// additional 實作 TokenClaims 介面
type additional struct {
additional map[string]string
}
func (use *additional) GetAll() map[string]string {
return use.additional
}
func (use *additional) Set(key token.Additional, val string) {
use.additional[key.String()] = val
}
func (use *additional) Get(additional token.Additional) string {
value, ok := use.additional[additional.String()]
if !ok {
return ""
}
return value
}
// NewAdditional 創建一個新的 tokenClaims 實例
func NewAdditional(data map[string]string) usecase.Additional {
return &additional{
additional: data,
}
}

View File

@ -0,0 +1,64 @@
package usecase
import (
"testing"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token"
"github.com/stretchr/testify/assert"
)
func TestAdditional_SetAndGet(t *testing.T) {
// 初始化 additional
additional := NewAdditional(map[string]string{})
// 測試 Set() 只允許有效 Key
validCases := map[token.Additional]string{
token.ID: "12345",
token.Role: "admin",
token.Device: "device-001",
token.UID: "user-999",
token.Account: "test@example.com",
token.Scope: "read:write",
}
// 測試有效 Key
for key, val := range validCases {
additional.Set(key, val)
assert.Equal(t, val, additional.Get(key), "Set/Get for key: "+key.String())
}
// 測試 key 未設定時應回傳空字串
assert.Equal(t, "", additional.Get("non-existent-key"))
}
func TestIsValidAdditional(t *testing.T) {
// 測試合法的 keys
assert.True(t, token.IsValidAdditional(token.ID))
assert.True(t, token.IsValidAdditional(token.Role))
assert.True(t, token.IsValidAdditional(token.Device))
assert.True(t, token.IsValidAdditional(token.UID))
assert.True(t, token.IsValidAdditional(token.Account))
assert.True(t, token.IsValidAdditional(token.Scope))
// 測試不合法的 keys
assert.False(t, token.IsValidAdditional(token.Additional("unknown")))
assert.False(t, token.IsValidAdditional(token.Additional("random")))
assert.False(t, token.IsValidAdditional(token.Additional("invalid-key")))
}
func TestIGetAll(t *testing.T) {
validCases := map[string]string{
token.ID.String(): "12345",
token.Role.String(): "admin",
token.Device.String(): "device-001",
token.UID.String(): "user-999",
token.Account.String(): "test@example.com",
token.Scope.String(): "read:write",
}
a := NewAdditional(validCases)
result := a.GetAll()
assert.Equal(t, validCases, result)
}

527
pkg/usecase/token.go Normal file
View File

@ -0,0 +1,527 @@
package usecase
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository"
dt "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/token"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase"
ers "code.30cm.net/digimon/library-go/errs"
"github.com/golang-jwt/jwt/v4"
"github.com/segmentio/ksuid"
"github.com/zeromicro/go-zero/core/logx"
)
type TokenUseCaseParam struct {
TokenRepo repository.TokenRepo
RefreshExpires time.Duration
Expired time.Duration
Secret string
}
type TokenUseCase struct {
TokenUseCaseParam
Token struct {
RefreshExpires time.Duration
Expired time.Duration
Secret string
}
}
func NewTokenUseCase(param TokenUseCaseParam) usecase.TokenUseCase {
return &TokenUseCase{
TokenUseCaseParam: param,
Token: struct {
RefreshExpires time.Duration
Expired time.Duration
Secret string
}{
RefreshExpires: param.RefreshExpires,
Expired: param.Expired,
Secret: param.Secret,
},
}
}
func (use *TokenUseCase) GenerateAccessToken(ctx context.Context, req usecase.GenerateTokenRequest) (usecase.AccessTokenResponse, error) {
token, err := use.newToken(ctx, &req)
if err != nil {
return usecase.AccessTokenResponse{}, err
}
err = use.TokenRepo.Create(ctx, *token)
if err != nil {
// 錯誤代碼
e := domain.TokenErrorL(
domain.TokenCreateErrorCode,
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: req},
{Key: "func", Value: "TokenRepo.Create"},
{Key: "err", Value: err.Error()},
},
"failed to create token").Wrap(err)
return usecase.AccessTokenResponse{}, e
}
return usecase.AccessTokenResponse{
AccessToken: token.AccessToken,
ExpiresIn: token.ExpiresIn,
RefreshToken: token.RefreshToken,
}, nil
}
func (use *TokenUseCase) RefreshAccessToken(ctx context.Context, req usecase.RefreshTokenRequest) (usecase.RefreshTokenResponse, error) {
// Step 1: 檢查 refresh token
token, err := use.TokenRepo.GetAccessTokenByOneTimeToken(ctx, req.Token)
if err != nil {
return usecase.RefreshTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.GetAccessTokenByOneTimeToken",
req: req,
err: err,
message: "failed to get access token",
errorCode: domain.TokenRefreshErrorCode,
})
}
// Step 2: 提取 Claims Data
claimsData, err := use.ParseSystemClaimsByAccessToken(token.AccessToken, use.Token.Secret, false)
if err != nil {
return usecase.RefreshTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "extractClaims",
req: req,
err: err,
message: "failed to extract claims",
errorCode: domain.TokenRefreshErrorCode,
})
}
data := NewAdditional(claimsData)
data.Set(dt.Scope, req.Scope)
data.Set(dt.Device, req.DeviceID)
// Step 3: 創建新 token
newToken, err := use.newToken(ctx, &usecase.GenerateTokenRequest{
Scope: req.Scope,
DeviceID: req.DeviceID,
Expires: req.Expires,
RefreshExpires: req.RefreshExpires,
Data: data.GetAll(),
Role: data.Get(dt.Role),
UID: data.Get(dt.UID),
Account: data.Get(dt.Account),
})
if err != nil {
return usecase.RefreshTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "use.newToken",
req: req,
err: err,
message: "failed to create new token",
errorCode: domain.TokenRefreshErrorCode,
})
}
if err := use.TokenRepo.Create(ctx, *newToken); err != nil {
return usecase.RefreshTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.Create",
req: req,
err: err,
message: "failed to create new token",
errorCode: domain.TokenRefreshErrorCode,
})
}
// Step 4: 刪除舊 token 並創建新 token
if err := use.TokenRepo.Delete(ctx, token); err != nil {
return usecase.RefreshTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.Delete",
req: req,
err: err,
message: "failed to delete old token",
errorCode: domain.TokenRefreshErrorCode,
})
}
// 返回新的 Token 響應
return usecase.RefreshTokenResponse{
AccessToken: newToken.AccessToken,
RefreshToken: newToken.RefreshToken,
ExpiresIn: newToken.ExpiresIn,
TokenType: data.Get(dt.Type),
}, nil
}
func (use *TokenUseCase) RevokeToken(ctx context.Context, req usecase.TokenRequest) error {
claims, err := use.ParseSystemClaimsByAccessToken(req.Token, use.Token.Secret, false)
if err != nil {
return use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "CancelToken extractClaims",
req: req,
err: err,
message: "failed to get token claims",
errorCode: domain.TokenCancelErrorCode,
})
}
data := NewAdditional(claims)
token, err := use.TokenRepo.GetAccessTokenByID(ctx, data.Get(dt.ID))
if err != nil {
return use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo GetAccessTokenByID",
req: req,
err: err,
message: fmt.Sprintf("failed to get token claims :%s", data.Get(dt.ID)),
errorCode: domain.TokenCancelErrorCode,
})
}
err = use.TokenRepo.Delete(ctx, token)
if err != nil {
return use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo Delete",
req: req,
err: err,
message: fmt.Sprintf("failed to delete token :%s", token.ID),
errorCode: domain.TokenCancelErrorCode,
})
}
return nil
}
func (use *TokenUseCase) VerifyToken(ctx context.Context, req usecase.TokenRequest) (usecase.VerifyTokenResponse, error) {
claims, err := use.ParseSystemClaimsByAccessToken(req.Token, use.Token.Secret, true)
if err != nil {
return usecase.VerifyTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "parseClaims",
req: req,
err: err,
message: "validate token claims error",
errorCode: domain.TokenValidateErrorCode,
})
}
data := NewAdditional(claims)
token, err := use.TokenRepo.GetAccessTokenByID(ctx, data.Get(dt.ID))
if err != nil {
return usecase.VerifyTokenResponse{},
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.GetAccessTokenByID",
req: req,
err: err,
message: fmt.Sprintf("failed to get token :%s", data.Get(dt.ID)),
errorCode: domain.TokenValidateErrorCode,
})
}
return usecase.VerifyTokenResponse{
Token: token,
Data: data.GetAll(),
}, nil
}
func (use *TokenUseCase) RevokeTokensByUID(ctx context.Context, req usecase.RevokeTokensByUIDRequest) error {
if req.UID != "" {
err := use.TokenRepo.DeleteAccessTokensByUID(ctx, req.UID)
if err != nil {
return use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.DeleteAccessTokensByUID",
req: req,
err: err,
message: "failed to cancel tokens by uid",
errorCode: domain.TokensCancelErrorCode,
})
}
}
if len(req.IDs) > 0 {
err := use.TokenRepo.DeleteAccessTokenByID(ctx, req.IDs)
if err != nil {
return use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.DeleteAccessTokenByID",
req: req,
err: err,
message: "failed to cancel tokens by token ids",
errorCode: domain.TokensCancelErrorCode,
})
}
}
return nil
}
func (use *TokenUseCase) RevokeTokensByDeviceID(ctx context.Context, deviceID string) error {
err := use.TokenRepo.DeleteAccessTokensByDeviceID(ctx, deviceID)
if err != nil {
return use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.DeleteAccessTokensByDeviceID",
req: deviceID,
err: err,
message: "failed to cancel token by device id",
errorCode: domain.TokensCancelErrorCode,
})
}
return nil
}
func (use *TokenUseCase) GetUserTokensByDeviceID(ctx context.Context, deviceID string) ([]*usecase.AccessTokenResponse, error) {
tokens, err := use.TokenRepo.GetAccessTokensByDeviceID(ctx, deviceID)
if err != nil {
return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.GetAccessTokensByDeviceID",
req: deviceID,
err: err,
message: "failed to get token by device id",
errorCode: domain.TokenGetErrorCode,
})
}
result := make([]*usecase.AccessTokenResponse, 0, len(tokens))
for _, v := range tokens {
result = append(result, &usecase.AccessTokenResponse{
AccessToken: v.AccessToken,
ExpiresIn: v.ExpiresIn,
RefreshToken: v.RefreshToken,
})
}
return result, nil
}
func (use *TokenUseCase) GetUserTokensByUID(ctx context.Context, uid string) ([]*usecase.AccessTokenResponse, error) {
tokens, err := use.TokenRepo.GetAccessTokensByUID(ctx, uid)
if err != nil {
return nil, use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "TokenRepo.GetAccessTokensByUID",
req: uid,
err: err,
message: "failed to get token by uid",
errorCode: domain.TokenGetErrorCode,
})
}
result := make([]*usecase.AccessTokenResponse, 0, len(tokens))
for _, v := range tokens {
result = append(result, &usecase.AccessTokenResponse{
AccessToken: v.AccessToken,
ExpiresIn: v.ExpiresIn,
RefreshToken: v.RefreshToken,
})
}
return result, nil
}
func (use *TokenUseCase) ReadTokenBasicData(ctx context.Context, token string) (usecase.Additional, error) {
claims, err := use.ParseSystemClaimsByAccessToken(token, use.Token.Secret, false)
if err != nil {
return nil,
use.wrapTokenError(ctx, wrapTokenErrorReq{
funcName: "parseClaims",
req: token,
err: err,
message: "validate token claims error",
errorCode: domain.TokenValidateErrorCode,
})
}
return NewAdditional(claims), nil
}
// ======== JWT Token ========
// CreateAccessToken 會將基本 token 以及想要加入Token Claims 的Data 依照 secret key 加密之後變成 jwt access token
func (use *TokenUseCase) CreateAccessToken(token entity.Token, data any, secretKey string) (string, error) {
claims := entity.Claims{
Data: data,
RegisteredClaims: jwt.RegisteredClaims{
ID: token.ID,
ExpiresAt: jwt.NewNumericDate(time.Unix(0, token.ExpiresIn)),
Issuer: dt.Issuer,
},
}
accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
SignedString([]byte(secretKey))
if err != nil {
return "", err
}
return accessToken, nil
}
func (use *TokenUseCase) CreateRefreshToken(accessToken string) string {
hash := sha256.New()
_, _ = hash.Write([]byte(accessToken))
return hex.EncodeToString(hash.Sum(nil))
}
func (use *TokenUseCase) ParseJWTClaimsByAccessToken(accessToken string, secret string, validate bool) (jwt.MapClaims, error) {
// 跳過驗證的解析
var token *jwt.Token
var err error
if validate {
token, err = jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("token unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return jwt.MapClaims{}, err
}
} else {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, err = parser.Parse(accessToken, func(_ *jwt.Token) (any, error) {
return []byte(secret), nil
})
if err != nil {
return jwt.MapClaims{}, err
}
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok && token.Valid {
return jwt.MapClaims{}, fmt.Errorf("token valid error")
}
return claims, nil
}
func (use *TokenUseCase) ParseSystemClaimsByAccessToken(accessToken string, secret string, validate bool) (map[string]string, error) {
claimMap, err := use.ParseJWTClaimsByAccessToken(accessToken, secret, validate)
if err != nil {
return map[string]string{}, err
}
claimsData, ok := claimMap["data"].(map[string]any)
if ok {
return convertMap(claimsData), nil
}
return map[string]string{}, fmt.Errorf("get data from claim map error")
}
// ======== 工具 ========
func (use *TokenUseCase) newToken(ctx context.Context, req *usecase.GenerateTokenRequest) (*entity.Token, error) {
// 準備建立 Token 所需
now := time.Now().UTC()
expires := req.Expires
refreshExpires := req.RefreshExpires
if expires <= 0 {
// 將時間加上 n 秒 -> 系統內預設
sec := time.Duration(use.Token.Expired.Seconds()) * time.Second
// 獲取 Unix 時間戳
expires = now.Add(sec).UnixNano()
}
// Refresh Token 過期時間要比普通的Token 長
if req.RefreshExpires <= 0 {
// 獲取 Unix 時間戳
refresh := time.Duration(use.Token.RefreshExpires.Seconds()) * time.Second
refreshExpires = now.Add(refresh).UnixNano()
}
token := entity.Token{
ID: ksuid.New().String(),
DeviceID: req.DeviceID,
ExpiresIn: expires,
RefreshExpiresIn: refreshExpires,
AccessCreateAt: now.UnixNano(),
RefreshCreateAt: now.UnixNano(),
UID: req.UID,
}
// 故意 data 裡面不會有那些已經有的欄位資訊
data := NewAdditional(req.Data)
data.Set(dt.ID, token.ID)
data.Set(dt.Role, req.Role)
data.Set(dt.Scope, req.Scope)
data.Set(dt.Account, req.Account)
data.Set(dt.UID, req.UID)
data.Set(dt.Type, req.TokenType)
if req.DeviceID != "" {
data.Set(dt.Device, req.DeviceID)
}
var err error
token.AccessToken, err = use.CreateAccessToken(token, data.GetAll(), use.Token.Secret)
token.RefreshToken = use.CreateRefreshToken(token.AccessToken)
if err != nil {
// 錯誤代碼 20-201-02
e := domain.TokenErrorL(
domain.TokenClaimErrorCode,
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: req},
{Key: "func", Value: "accessTokenGenerator"},
{Key: "err", Value: err.Error()},
},
"failed to generator access token").Wrap(err)
return nil, e
}
return &token, nil
}
func convertMap(input map[string]any) map[string]string {
output := make(map[string]string)
for key, value := range input {
switch v := value.(type) {
case string:
output[key] = v
case fmt.Stringer:
output[key] = v.String()
default:
output[key] = fmt.Sprintf("%v", value)
}
}
return output
}
type wrapTokenErrorReq struct {
funcName string
req any
err error
message string
errorCode ers.ErrorCode
}
// wrapTokenError 將錯誤訊息封裝到 domain.TokenErrorL 中
func (use *TokenUseCase) wrapTokenError(ctx context.Context, param wrapTokenErrorReq) error {
logFields := []logx.LogField{
{Key: "req", Value: param.req},
{Key: "func", Value: param.funcName},
{Key: "err", Value: param.err.Error()},
}
wrappedErr := domain.TokenErrorL(
param.errorCode,
logx.WithContext(ctx),
logFields,
param.message,
).Wrap(param.err)
return wrappedErr
}

611
pkg/usecase/token_test.go Normal file
View File

@ -0,0 +1,611 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase"
mock "code.30cm.net/digimon/app-cloudep-permission-server/pkg/mock/repository"
"context"
"fmt"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"testing"
"time"
)
// TestTokenUseCase_CreateAccessToken_TableDriven 透過 table-driven 方式測試 CreateAccessToken
func TestTokenUseCase_CreateAccessToken_TableDriven(t *testing.T) {
// 固定發行者設定,測試中會用來驗證 claims.Issuer
now := time.Now()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockAutoIDModel := mock.NewMockTokenRepo(mockCtrl)
uc := NewTokenUseCase(TokenUseCaseParam{
TokenRepo: mockAutoIDModel,
RefreshExpires: 2 * time.Minute,
Expired: 2 * time.Minute,
Secret: "gg88g88",
})
tests := []struct {
name string
token entity.Token
data any
secretKey string
wantErr bool
verifyClaims func(t *testing.T, claims *entity.Claims, expectedExpiry time.Time)
}{
{
name: "ok",
token: entity.Token{
ID: "token1",
ExpiresIn: now.Add(1 * time.Hour).UnixNano(),
},
data: map[string]interface{}{"foo": "bar"},
secretKey: "secret123",
wantErr: false,
verifyClaims: func(t *testing.T, claims *entity.Claims, expectedExpiry time.Time) {
assert.Equal(t, "token1", claims.ID)
assert.True(t, claims.ExpiresAt.Time.Before(expectedExpiry),
"expected expiry %v, got %v", expectedExpiry, claims.ExpiresAt.Time)
dataMap, ok := claims.Data.(map[string]interface{})
assert.True(t, ok, "claims.Data 應為 map[string]interface{}")
assert.Equal(t, "bar", dataMap["foo"])
},
},
{
name: "valid token with string data",
token: entity.Token{
ID: "token2",
ExpiresIn: now.Add(2 * time.Hour).UnixNano(),
},
data: map[string]interface{}{"foo": "bar"},
secretKey: "anotherSecret",
wantErr: false,
verifyClaims: func(t *testing.T, claims *entity.Claims, expectedExpiry time.Time) {
assert.Equal(t, "token2", claims.ID)
assert.True(t, claims.ExpiresAt.Time.Before(expectedExpiry))
assert.Equal(t, map[string]interface{}{"foo": "bar"}, claims.Data)
},
},
{
name: "empty secret key",
token: entity.Token{
ID: "token3",
ExpiresIn: now.Add(30 * time.Minute).UnixNano(),
},
data: map[string]interface{}{"key": "value"},
secretKey: "",
wantErr: false,
verifyClaims: func(t *testing.T, claims *entity.Claims, expectedExpiry time.Time) {
assert.Equal(t, "token3", claims.ID)
assert.True(t, claims.ExpiresAt.Time.Before(expectedExpiry))
dataMap, ok := claims.Data.(map[string]interface{})
assert.True(t, ok, "claims.Data 應為 map[string]interface{}")
assert.Equal(t, "value", dataMap["key"])
},
},
// 如有需要,可加入更多測試案例,例如模擬簽名錯誤等情境
}
for _, tt := range tests {
tt := tt // 捕捉範圍變數
t.Run(tt.name, func(t *testing.T) {
jwtStr, err := uc.CreateAccessToken(tt.token, tt.data, tt.secretKey)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// 解析 JWT
parsedToken, err := jwt.ParseWithClaims(jwtStr, &entity.Claims{}, func(token *jwt.Token) (interface{}, error) {
// 驗證簽名方法是否為 HMAC
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(tt.secretKey), nil
})
assert.NoError(t, err)
assert.True(t, parsedToken.Valid, "解析後的 JWT 應該有效")
claims, ok := parsedToken.Claims.(*entity.Claims)
assert.True(t, ok, "claims 型別錯誤,預期 *entity.Claims, got %T", parsedToken.Claims)
// 根據 token.ExpiresIn 計算預期的過期時間
expectedExpiry := time.Unix(0, tt.token.ExpiresIn)
// 呼叫 verifyClaims 驗證其它 Claim 資料
tt.verifyClaims(t, claims, expectedExpiry)
})
}
}
func TestTokenUseCase_CreateRefreshToken(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockAutoIDModel := mock.NewMockTokenRepo(mockCtrl)
uc := NewTokenUseCase(TokenUseCaseParam{
TokenRepo: mockAutoIDModel,
RefreshExpires: 2 * time.Minute,
Expired: 2 * time.Minute,
Secret: "gg88g88",
})
tests := []struct {
name string
accessToken string
expected string
}{
{
name: "empty access token",
accessToken: "",
// SHA256("") 的 hex 編碼結果
expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "normal access token",
accessToken: "access-token",
expected: "3f16bed7089f4653e5ef21bfd2824d7f3aaaecc7a598e7e89c580e1606a9cc52",
},
}
for _, tt := range tests {
tt := tt // 捕捉變數
t.Run(tt.name, func(t *testing.T) {
result := uc.CreateRefreshToken(tt.accessToken)
assert.Equal(t, tt.expected, result)
})
}
}
// TestTokenUseCase_ParseJWTClaimsByAccessToken 使用 table-driven 方式測試 ParseJWTClaimsByAccessToken
func TestTokenUseCase_ParseJWTClaimsByAccessToken(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockAutoIDModel := mock.NewMockTokenRepo(mockCtrl)
uc := NewTokenUseCase(TokenUseCaseParam{
TokenRepo: mockAutoIDModel,
RefreshExpires: 2 * time.Minute,
Expired: 2 * time.Minute,
Secret: "gg88g88",
})
// 定義測試案例的結構
tests := []struct {
name string
// tokenGen 用來動態產生要解析的 access token
tokenGen func(t *testing.T) string
secret string
validate bool
wantClaims jwt.MapClaims
wantErr bool
errContains string
}{
{
name: "valid token with validation",
tokenGen: func(t *testing.T) string {
claims := jwt.MapClaims{
"sub": "123",
"role": "admin",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte("testsecret"))
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenString
},
secret: "testsecret",
validate: true,
wantClaims: jwt.MapClaims{
"sub": "123",
"role": "admin",
},
wantErr: false,
},
{
name: "valid token without validation",
tokenGen: func(t *testing.T) string {
claims := jwt.MapClaims{
"sub": "123",
"role": "admin",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte("testsecret"))
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenString
},
secret: "testsecret",
validate: false,
wantClaims: jwt.MapClaims{
"sub": "123",
"role": "admin",
},
wantErr: false,
},
{
name: "invalid secret",
tokenGen: func(t *testing.T) string {
claims := jwt.MapClaims{
"sub": "123",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte("testsecret"))
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenString
},
secret: "wrongsecret",
validate: true,
wantErr: true,
errContains: "signature", // 預期錯誤訊息中包含 "signature"
},
{
name: "unexpected signing method",
tokenGen: func(t *testing.T) string {
claims := jwt.MapClaims{
"sub": "456",
}
// 使用 SigningMethodNone 產生 token
token := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
// 針對 None 演算法SignedString 需要使用 jwt.UnsafeAllowNoneSignatureType
tokenString, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenString
},
secret: "testsecret",
validate: true,
wantErr: true,
errContains: "unexpected signing method",
},
{
name: "malformed token",
tokenGen: func(t *testing.T) string {
return "not-a-token"
},
secret: "testsecret",
validate: true,
wantErr: true,
errContains: "token contains an invalid number of segments",
},
}
// 針對每個測試案例執行測試
for _, tt := range tests {
tt := tt // 捕捉迴圈變數
t.Run(tt.name, func(t *testing.T) {
// 產生 access token
accessToken := tt.tokenGen(t)
claims, err := uc.ParseJWTClaimsByAccessToken(accessToken, tt.secret, tt.validate)
if tt.wantErr {
assert.Error(t, err)
if err != nil && tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
assert.NoError(t, err)
// 驗證解析出來的 claims 是否符合預期
assert.Equal(t, tt.wantClaims, claims)
})
}
}
func TestTokenUseCase_ParseSystemClaimsByAccessToken(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockAutoIDModel := mock.NewMockTokenRepo(mockCtrl)
uc := NewTokenUseCase(TokenUseCaseParam{
TokenRepo: mockAutoIDModel,
RefreshExpires: 2 * time.Minute,
Expired: 2 * time.Minute,
Secret: "gg88g88",
})
//table-driven 測試案例
tests := []struct {
name string
tokenGen func(t *testing.T) string // 用來產生 access token
secret string
validate bool
want map[string]string // 預期轉換後的資料
wantErr bool
errContains string
}{
{
name: "valid token with correct data map",
tokenGen: func(t *testing.T) string {
// 建立 claims其中 "data" 欄位為 map[string]any
claims := jwt.MapClaims{
"data": map[string]any{
"key1": "value1",
"key2": "value2",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte("secret"))
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenStr
},
secret: "secret",
validate: true,
want: map[string]string{
"key1": "value1",
"key2": "value2",
},
wantErr: false,
},
{
name: "token missing data field",
tokenGen: func(t *testing.T) string {
// claims 中不包含 "data"
claims := jwt.MapClaims{
"other": "something",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte("secret"))
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenStr
},
secret: "secret",
validate: true,
want: map[string]string{},
wantErr: true,
errContains: "get data from claim map error",
},
{
name: "malformed token",
tokenGen: func(t *testing.T) string {
return "not-a-token"
},
secret: "secret",
validate: true,
want: map[string]string{},
wantErr: true,
errContains: "token contains an invalid number of segments",
},
{
name: "data field not a map",
tokenGen: func(t *testing.T) string {
// 將 "data" 設為一個字串,而非 map
claims := jwt.MapClaims{
"data": "not-a-map",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte("secret"))
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
return tokenStr
},
secret: "secret",
validate: true,
want: map[string]string{},
wantErr: true,
errContains: "get data from claim map error",
},
}
for _, tt := range tests {
tt := tt // 捕捉區域變數
t.Run(tt.name, func(t *testing.T) {
accessToken := tt.tokenGen(t)
result, err := uc.ParseSystemClaimsByAccessToken(accessToken, tt.secret, tt.validate)
if tt.wantErr {
assert.Error(t, err)
if err != nil && tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, result)
})
}
}
func TestTokenUseCase_newToken(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockAutoIDModel := mock.NewMockTokenRepo(mockCtrl)
uc := TokenUseCase{
TokenUseCaseParam: TokenUseCaseParam{
TokenRepo: mockAutoIDModel,
RefreshExpires: 2 * time.Minute,
Expired: 2 * time.Minute,
Secret: "gg88g88",
},
}
// 取得一個參考時間,用來檢查 default expiration 的結果
nowRef := time.Now().UTC()
tests := []struct {
name string
req *usecase.GenerateTokenRequest
// 模擬產生 AccessToken 與 RefreshToken 的函式
stubAccessToken func(token entity.Token, data map[string]interface{}, secret string) (string, error)
stubRefreshToken func(accessToken string) string
wantErr bool
// 當使用者提供明確的 expires 與 refreshExpires 時,期望的值(否則使用預設)
expectExpiresProvided bool
expectedExpires int64
expectRefreshExpiresProvided bool
expectedRefreshExpires int64
}{
{
name: "default expiration used when req.Expires/RefreshExpires are zero",
req: &usecase.GenerateTokenRequest{
DeviceID: "device1",
UID: "user1",
Expires: 0,
RefreshExpires: 0,
Data: map[string]string{"foo": "bar"},
Role: "admin",
Scope: "read",
Account: "account1",
TokenType: "access",
},
wantErr: false,
expectExpiresProvided: false,
expectRefreshExpiresProvided: false,
},
{
name: "explicit expiration provided",
req: func() *usecase.GenerateTokenRequest {
// 提供明確的 expires 與 refreshExpires
exp := nowRef.Add(5 * time.Minute).UnixNano()
refExp := nowRef.Add(10 * time.Minute).UnixNano()
return &usecase.GenerateTokenRequest{
DeviceID: "device2",
UID: "user2",
Expires: exp,
RefreshExpires: refExp,
Data: map[string]string{},
Role: "user",
Scope: "write",
Account: "account2",
TokenType: "access",
}
}(),
stubAccessToken: func(token entity.Token, data map[string]interface{}, secret string) (string, error) {
return "access-token", nil
},
stubRefreshToken: func(accessToken string) string {
return "refresh-token"
},
wantErr: false,
expectExpiresProvided: true,
// 預期值就與 req.Expires 相同
expectedExpires: func() int64 { return nowRef.Add(5 * time.Minute).UnixNano() }(),
expectRefreshExpiresProvided: true,
expectedRefreshExpires: func() int64 { return nowRef.Add(10 * time.Minute).UnixNano() }(),
},
}
for _, tt := range tests {
tt := tt // 捕捉範圍變數
t.Run(tt.name, func(t *testing.T) {
// 呼叫 newToken 方法
token, err := uc.newToken(context.Background(), tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// 檢查基本欄位
assert.NotEmpty(t, token.ID, "token.ID should not be empty")
assert.Equal(t, tt.req.DeviceID, token.DeviceID)
assert.Equal(t, tt.req.UID, token.UID)
// 驗證建立時間欄位有被設置
assert.NotZero(t, token.AccessCreateAt)
assert.NotZero(t, token.RefreshCreateAt)
})
}
}
func TestTokenUseCase_GenerateAccessToken(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockNewMockTokenRepo := mock.NewMockTokenRepo(mockCtrl)
uc := TokenUseCase{
TokenUseCaseParam: TokenUseCaseParam{
TokenRepo: mockNewMockTokenRepo,
RefreshExpires: 2 * time.Minute,
Expired: 2 * time.Minute,
Secret: "gg88g88",
},
}
// 定義 table-driven 測試案例
tests := []struct {
name string
repoErr error
req usecase.GenerateTokenRequest
wantErr bool
errContains string
setup func()
// 若成功,預期回傳的 access token 與 refresh token
expectedAccessToken string
expectedRefreshToken string
}{
{
name: "newToken error from CreateAccessToken",
repoErr: nil,
setup: func() {
mockNewMockTokenRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(fmt.Errorf("token create error: failed to create token"))
},
req: usecase.GenerateTokenRequest{
DeviceID: "device1",
UID: "user1",
Expires: 0, // 使用預設過期時間
RefreshExpires: 0,
Data: map[string]string{"foo": "bar"},
Role: "admin",
Scope: "read",
Account: "account1",
TokenType: "access",
},
wantErr: true,
errContains: "token create error: failed to create token",
},
{
name: "successful generation",
repoErr: nil,
setup: func() {
mockNewMockTokenRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)
},
req: usecase.GenerateTokenRequest{
DeviceID: "device3",
UID: "user3",
Expires: 0,
RefreshExpires: 0,
Data: map[string]string{"foo": "bar"},
Role: "member",
Scope: "read",
Account: "account3",
TokenType: "access",
},
wantErr: false,
},
}
// 針對每個測試案例執行測試
for _, tt := range tests {
tt := tt // 捕捉區域變數
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
tt.setup()
resp, err := uc.GenerateAccessToken(ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
if err != nil && tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
assert.NoError(t, err)
// 驗證 ExpiresIn 非零newToken 會根據當前時間與設定產生過期時間)
assert.NotZero(t, resp.ExpiresIn)
})
}
}