diff --git a/Makefile b/Makefile index 777c00b..30f9486 100644 --- a/Makefile +++ b/Makefile @@ -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" diff --git a/build/Dockerfile b/build/Dockerfile index 844dc71..99fbad8 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -2,7 +2,7 @@ # BUILDER # ########### -FROM golang:1.23.4 AS builder +FROM golang:1.24.0 AS builder ARG VERSION ARG BUILT diff --git a/generate/protobuf/permission.proto b/generate/protobuf/permission.proto index 148c629..62f547e 100644 --- a/generate/protobuf/permission.proto +++ b/generate/protobuf/permission.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package permission; -option go_package="./app-cloudep-permission-server"; +option go_package="./permission"; // OKResp message OKResp {} diff --git a/go.mod b/go.mod index b0bd8e2..9d27328 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 6c845c5..c011fc5 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/logic/tokenservice/cancel_one_time_token_logic.go b/internal/logic/tokenservice/cancel_one_time_token_logic.go index 9bd1b34..3a0376c 100644 --- a/internal/logic/tokenservice/cancel_one_time_token_logic.go +++ b/internal/logic/tokenservice/cancel_one_time_token_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/cancel_token_by_device_id_logic.go b/internal/logic/tokenservice/cancel_token_by_device_id_logic.go index fb97add..7a3a63a 100644 --- a/internal/logic/tokenservice/cancel_token_by_device_id_logic.go +++ b/internal/logic/tokenservice/cancel_token_by_device_id_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/cancel_token_logic.go b/internal/logic/tokenservice/cancel_token_logic.go index 1e0cee0..322f8d6 100644 --- a/internal/logic/tokenservice/cancel_token_logic.go +++ b/internal/logic/tokenservice/cancel_token_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/cancel_tokens_logic.go b/internal/logic/tokenservice/cancel_tokens_logic.go index 323224e..2e1a0d3 100644 --- a/internal/logic/tokenservice/cancel_tokens_logic.go +++ b/internal/logic/tokenservice/cancel_tokens_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go b/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go index 134d249..3b53d91 100644 --- a/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go +++ b/internal/logic/tokenservice/get_user_tokens_by_device_id_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go b/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go index 758b407..1763389 100644 --- a/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go +++ b/internal/logic/tokenservice/get_user_tokens_by_uid_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/new_one_time_token_logic.go b/internal/logic/tokenservice/new_one_time_token_logic.go index d4736be..0cda13c 100644 --- a/internal/logic/tokenservice/new_one_time_token_logic.go +++ b/internal/logic/tokenservice/new_one_time_token_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/new_token_logic.go b/internal/logic/tokenservice/new_token_logic.go index bbab347..3ffad34 100644 --- a/internal/logic/tokenservice/new_token_logic.go +++ b/internal/logic/tokenservice/new_token_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/refresh_token_logic.go b/internal/logic/tokenservice/refresh_token_logic.go index 22042f4..fb9fef2 100644 --- a/internal/logic/tokenservice/refresh_token_logic.go +++ b/internal/logic/tokenservice/refresh_token_logic.go @@ -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 } diff --git a/internal/logic/tokenservice/validation_token_logic.go b/internal/logic/tokenservice/validation_token_logic.go index 0f682e1..e4f36c6 100644 --- a/internal/logic/tokenservice/validation_token_logic.go +++ b/internal/logic/tokenservice/validation_token_logic.go @@ -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 } diff --git a/internal/server/tokenservice/token_service_server.go b/internal/server/tokenservice/token_service_server.go index b662fa8..e0a53ae 100644 --- a/internal/server/tokenservice/token_service_server.go +++ b/internal/server/tokenservice/token_service_server.go @@ -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) } diff --git a/permission.go b/permission.go index 0b614d3..c0be72c 100644 --- a/permission.go +++ b/permission.go @@ -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) diff --git a/pkg/domain/entity/claims.go b/pkg/domain/entity/claims.go new file mode 100644 index 0000000..9271795 --- /dev/null +++ b/pkg/domain/entity/claims.go @@ -0,0 +1,8 @@ +package entity + +import "github.com/golang-jwt/jwt/v4" + +type Claims struct { + jwt.RegisteredClaims + Data interface{} `json:"data"` +} diff --git a/pkg/domain/error.go b/pkg/domain/error.go index 4188b5a..c925ecf 100644 --- a/pkg/domain/error.go +++ b/pkg/domain/error.go @@ -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 +} diff --git a/pkg/domain/repository/token.go b/pkg/domain/repository/token.go index f19e7da..4c2d1f2 100644 --- a/pkg/domain/repository/token.go +++ b/pkg/domain/repository/token.go @@ -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 diff --git a/pkg/domain/token/additional.go b/pkg/domain/token/additional.go new file mode 100644 index 0000000..d13874a --- /dev/null +++ b/pkg/domain/token/additional.go @@ -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" +) diff --git a/pkg/domain/token/scope.go b/pkg/domain/token/scope.go new file mode 100644 index 0000000..b44923f --- /dev/null +++ b/pkg/domain/token/scope.go @@ -0,0 +1,7 @@ +package token + +type TScope string + +func (s *TScope) ToString() string { + return string(*s) +} diff --git a/pkg/domain/token/type.go b/pkg/domain/token/type.go new file mode 100644 index 0000000..6ac8f0a --- /dev/null +++ b/pkg/domain/token/type.go @@ -0,0 +1,11 @@ +package token + +type VerifyType string + +func (t *VerifyType) ToString() string { + return string(*t) +} + +const ( + Bearer VerifyType = "Bearer" +) diff --git a/pkg/domain/usecase/additional.go b/pkg/domain/usecase/additional.go new file mode 100644 index 0000000..e7f6b29 --- /dev/null +++ b/pkg/domain/usecase/additional.go @@ -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 +} diff --git a/pkg/domain/usecase/token.go b/pkg/domain/usecase/token.go index 1e15c7a..59f9972 100644 --- a/pkg/domain/usecase/token.go +++ b/pkg/domain/usecase/token.go @@ -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"` // 授權類型 - 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"` // 是否為刷新令牌 - Role string `json:"role"` // 是否為刷新令牌 - Account string `json:"account"` // 登入時的帳號 +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"` // 授權範圍 + Expires int64 `json:"expires"` // 指定過期時間 UnixNano UTC 時間(沒給 = now 加設定秒數) + RefreshExpires int64 `json:"refresh_expires"` // 指定過期時間 UnixNano UTC 時間(沒給 = now 加設定秒數) + Role string `json:"role"` // 是否為刷新令牌 + 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 } diff --git a/pkg/mock/repository/token.go b/pkg/mock/repository/token.go new file mode 100644 index 0000000..70dfa13 --- /dev/null +++ b/pkg/mock/repository/token.go @@ -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) +} diff --git a/pkg/repository/token.go b/pkg/repository/token.go index 75c809b..0ab2ef5 100644 --- a/pkg/repository/token.go +++ b/pkg/repository/token.go @@ -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 需要的參數 diff --git a/pkg/repository/token_test.go b/pkg/repository/token_test.go index 2984740..8612dc3 100644 --- a/pkg/repository/token_test.go +++ b/pkg/repository/token_test.go @@ -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) { diff --git a/pkg/usecase/additional.go b/pkg/usecase/additional.go new file mode 100644 index 0000000..6bd2c1e --- /dev/null +++ b/pkg/usecase/additional.go @@ -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, + } +} diff --git a/pkg/usecase/additional_test.go b/pkg/usecase/additional_test.go new file mode 100644 index 0000000..8f332ce --- /dev/null +++ b/pkg/usecase/additional_test.go @@ -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) + +} diff --git a/pkg/usecase/token.go b/pkg/usecase/token.go new file mode 100644 index 0000000..60c1338 --- /dev/null +++ b/pkg/usecase/token.go @@ -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 +} diff --git a/pkg/usecase/token_test.go b/pkg/usecase/token_test.go new file mode 100644 index 0000000..757d56c --- /dev/null +++ b/pkg/usecase/token_test.go @@ -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) + }) + } +}